Files
Nitro-V3/docs/SESSION_2026-05-18_changelog.md
T
simoleo89 2db6df71a9 docs(changelog): append Phase 12 — snapshot consumer wiring + first migrations
Extend SESSION_2026-05-18_changelog.md with the five-commit Phase 12
batch landed after the original changelog (e7e8bcc) was written:

- b2a86da: feat(hooks/session) — eight consumer hooks for the renderer
  snapshot pattern (useUserDataSnapshot, useActiveRoomSessionSnapshot,
  useIgnoredUsersSnapshot + useIsUserIgnored, useGroupBadgesSnapshot +
  useGroupBadge, useVolumesSnapshot, useRoomUserListSnapshot).
- 71a0eee: refactor(hooks/session) — migrate useSessionInfo's userFigure
  / respects mirrors to useUserDataSnapshot; drop 3 useState + 2
  useMessageEvent.
- 36addbe: fix(avatar-info) — reactive Ignore/Unignore menu entry; the
  previously-stale captured boolean now flips in real time via
  useIsUserIgnored.
- 02a396d: docs(CLAUDE.md) refresh — bump Vitest 193→203, drop obsolete
  rows, document the new snapshot-consumer pattern, mark the two old
  open logic bugs as closed.

Also bumps the headline commit count 109 → 114 and updates the
end-of-session HEAD references.
2026-05-18 21:37:11 +02:00

69 KiB
Raw Blame History

React 19 modernization branches — complete changelog

End-to-end documentation of every modification made since the two modernization branches were opened:

  • Nitro-V3 (React client) — branch feat/react19-modernization, 114 commits since baseline ae17619
  • Nitro_Render_V3 (renderer library) — branch feat/react19-event-bus, 22 commits since baseline 98b03aa

Plus the in-session upstream sync of the third codebase touched on 2026-05-18:

  • Arcturus-Morningstar-Extended (Java emulator) — FF pull e6093f9efb4997 (v4.1.16)

Working directory: E:\Users\simol\Desktop\DEV. (NitroV3-Housekeeping was not touched during the lifetime of these branches.)


Table of contents

  1. Overview
  2. Nitro-V3 client — branch story
  3. Nitro_Render_V3 renderer — branch story
  4. Arcturus emulator — upstream pull
  5. Documentation evolution (CLAUDE.md / ARCHITECTURE.md)
  6. Full commit index
  7. Final state matrix

1. Overview

Branches and their goals

feat/react19-modernization (Nitro-V3 client) was opened to bring the React client up to React 19 idioms and the supporting infrastructure that React 19 unlocks: TanStack Query for server state, Zustand for cross-component UI state, Vitest for unit testing, React Compiler for automatic memoization, and react-error-boundary for graceful degradation. Along the way it absorbed god-hook decompositions, file extractions on oversized components, Pixi v8 alignment, two upstream cherry-picks (duckietm PR #126), an open-upstream PR (#130 — toolbar spam-toggle fix), and finally a full sync of origin/Dev through b2318b9.

feat/react19-event-bus (Nitro_Render_V3 renderer) was opened to add React-friendly primitives to the renderer library so the client could consume it through useSyncExternalStore, use(), and TanStack Query without re-architecting the event bus. It then absorbed TypeScript 6 + tsgo migration, TS 5.7+ ArrayBuffer drift fixes, Pixi v8 type alignment, composer/parser alignment with Arcturus (RoomEnterComposer, RoomSettingsData.allowUnderpass, etc.), dead-code removal, and finally — in the 2026-05-18 session — four new snapshot-pattern extensions covering ignored users, group badges, the room user list, and sound volumes.

Current state

Branch HEAD Commits since baseline Typecheck Vitest
Nitro-V3 / feat/react19-modernization 02a396d 114 (baseline ae17619) clean 203/203
Nitro_Render_V3 / feat/react19-event-bus 28c552f 22 (baseline 98b03aa) clean 127/127
Arcturus / main efb4997 (v4.1.16) tracks origin/main with no local divergence n/a n/a

Key architectural decisions taken

  1. Stay on the classic src/components/ + src/hooks/ layout — an early experiment with src/features/<feature>/ was reverted (commit 0755285); the team decided the in-place layout is the convention. Every PR that violates it gets reworked.
  2. God-hook split into 3 files, flat in the hooks barrel directoryuse<Feature>State, use<Feature>Actions, use<Feature>Widget (deprecated wrapper). No per-feature subfolders for hooks.
  3. useBetween-based singleton-filter pattern — for hooks shared by many consumers but where most consumers only need state OR actions, not both: one internal useBetweenStore, then a useState filter, useActions filter, and a deprecated useFoo shim.
  4. Renderer-SDK mock for Vitestsrc/nitro-renderer.mock.ts aliased over @nitrots/nitro-renderer via vitest.config.mts. Without it, importing any src/api/* file in a test crashes jsdom because the real renderer eagerly loads Pixi v8 at module-import time.
  5. Tests co-located next to subjectssrc/path/Foo.tssrc/path/Foo.test.ts. No parallel tests/ tree.
  6. Snapshot pattern on the renderer — referentially-stable, lazy-frozen, invalidated-on-mutation. Now extended to 6 state holders (Session, RoomSession, IgnoredUsers, GroupInfo, RoomUserList, SoundVolumes).
  7. Composer/parser optional trailing fields use a flat early-return chain, never nested if(bytesAvailable) guards. The pattern is now documented in the renderer's CLAUDE.md.

Commit author + signing convention

All commits authored as simoleo89 <simoleo89@users.noreply.github.com> (client) or simoleo89 <simoleo89@gmail.com> (renderer), passed via per-command git -c user.name=… -c user.email=… overrides — the global git config is never modified. Co-authored-by trailers are explicitly forbidden by a feedback memory entry.


2. Nitro-V3 client — branch story

Phase 1: React 19 baseline adoption

The runtime was already on react@19.2.5 but no React 19 APIs were in use. Phase 1 brought the codebase forward to idiomatic React 19.

a1bee1d — Initial React 19 modernization sweep

  • forwardRef → ref-as-prop in 7 layout/component files (NitroInput, Button, ItemCountBadge, Card variants, InfiniteGridItem, ToolbarItemView, AvatarEditorIcon).
  • <Context.Provider><Context> in 6 contexts (CatalogAdmin, FloorplanEditor, UiSettings, GridContext, NitroCardContext, NitroCardAccordionContext).
  • Native <script> hoisting for Turnstile, ExternalPluginLoader, GoogleAdsView. React 19 dedupes by src and removes manual document.head.appendChild + module-level promise caches.
  • React Compiler enabled at build time via babel-plugin-react-compiler in vite.config.mjs (target '19'), plus eslint-plugin-react-compiler in lint mode.
  • Global <ErrorBoundary> + <Suspense> in src/index.tsx using react-error-boundary, with LoadingView as fallback.
  • use(promise) pilot in BackgroundsView demonstrating Suspense-driven config loading.
  • ESLint react settings bumped 18.3.1 → 19.2; legacy @typescript-eslint/ban-types replaced with no-restricted-types (the old rule was removed in @typescript-eslint v8).

Form Actions phase (login/register) deferred to its own commit because LoginView.tsx was 1623 lines with lockout + Turnstile + heartbeat interleaving.

1b1e0c1 — React 19 Phase 3: login/forgot/register forms migrated to Form Actions

  • Login formhandleLoginSubmit → loginAction(prevState, FormData) wrapped in useActionState. Submit button extracted as <LoginSubmitButton/> reading pending state via useFormStatus. Reads username/password/remember from FormData; remember checkbox carries name="remember".
  • Forgot password formforgotAction wrapped in useActionState; awaits parent onSubmit so pending stays true through the parent fetch. ForgotSubmitButton uses useFormStatus.
  • Register credentials stepcredentialsAction with useActionState; the step transition (setStep('avatar')) happens inside the action after pingServer + onCheckEmail.
  • Register avatar stepavatarAction validates username, pings server, checks availability, then awaits onSubmit. Button label uses isAvatarPending to show "Creating…" without prop drilling submitting.
  • DialogSharedProps.onSubmit signatures updated to return Promise<void> so dialog actions can await the parent's fetch.
  • lockState memo replaced with a direct readLock() call in render — any re-render (triggered by the action's pending toggle) recomputes it.
  • Unused FormEvent import dropped; unused checking state in RegisterDialog dropped.

Other Phase 1 commits

  • 535fa71 — ESLint --fix auto-fix for brace-style/indent/semi/no-trailing-spaces (mechanical hygiene before adopting new lint rules).
  • 25d51af — Enabled <StrictMode> + made App.tsx renderer init idempotent (StrictMode double-invokes effects in dev — the renderer init had to become safe to run twice).
  • 13dc483 — Bumped ecosystem dependencies (minor/patch).
  • 5697d16 — Fixed rules-of-hooks violation in InfiniteGrid.
  • 6c9f414 — Applied useEffectEvent (React 19.2) to TurnstileWidget callbacks (stops stale-closure issues without bloating effect deps).
  • f18c917 — Added @typescript/native-preview (tsgo, the TS 7 preview compiler) as a fast yarn typecheck script alongside TS 6.
  • d382635 — Phase A: cleared all react-hooks/exhaustive-deps warnings via useEffectEvent or hoisting.
  • 39eb2c6 — Phase C: cleared 4 set-state-in-effect violations on safe candidates.

Phase 2: Infrastructure pillars (Query, Zustand, Vitest, mocks, Form Actions)

The next chunk laid down the long-term infrastructure that the rest of the modernization rests on.

48d62c5 — Architecture refactor: docs + 5 pilot implementations + error boundary

Introduced docs/ARCHITECTURE.md (~370 lines) — a living document describing where the project stood, five proposed structural improvements (feature-folder migration, TanStack Query, Zustand, god-hook splits, Vitest), and the recommended order for the next refactor PRs. Concrete pilots delivered alongside:

  • Doorbell feature foldersrc/features/doorbell/ with views/, hooks/useDoorbellState.ts, hooks/useDoorbellActions.ts. The split (data vs actions) became the canonical pattern. The folder structure itself was later reverted (commit 0755285); the data/actions split survived.

0755285 — Reverted feature-folder migration; kept classic src/components/ + src/hooks/

Team feedback on the experiment was that src/features/<feature>/ introduced cross-cutting friction without obvious wins. Reverted the folder migration but kept the split convention. From this point onward, every god-hook split lives in src/hooks/<area>/use<Feature>State.ts + use<Feature>Actions.ts + use<Feature>Widget.ts (deprecated shim).

34b1b56 — Enable TanStack Query (proposal #2) + first real-data pilot on OfferView

  • yarn add @tanstack/react-query@5 @tanstack/react-query-devtools@5 (^5 matches React 19 peer).
  • src/index.tsx mounts QueryClientProvider above ErrorBoundary + Suspense. Default config: staleTime=30s, retry=1, refetchOnWindowFocus=false (chat client, not a data dashboard).
  • New adapter at src/api/nitro-query/createNitroQuery.ts. Exposes useNitroQuery({ key, request, parser, select, timeoutMs }) (wraps TanStack's useQuery) and awaitNitroResponse(...) (lower-level helper). The internal promise registers the parser, dispatches the composer, resolves with select(event) on first matching parser, rejects after timeoutMs (default 15s), and always cleans up.
  • First pilotOfferView migrated from the previous useMessageEventState + manual useEffect-send pattern.

fd1835c — Enable Zustand (proposal #5) + convert isCreatingRoom singleton

  • yarn add zustand (^5).
  • src/state/createNitroStore.ts exports a re-export of zustand's create under the project-local name createNitroStore. Comments document the convention (one store per domain, subscribe to slices not the whole store).
  • First migration targetsrc/components/navigator/views/navigatorRoomCreatorStore.ts. A Zustand store with isCreating: boolean and beginCreate() (latches the flag, dispatches an auto-reset setTimeout after 5s, replaces any in-flight timer on re-entry). The component drops two module-level let variables that React Compiler was flagging.

6793de2 — Set up Vitest + 22 smoke tests on pure modules (proposal #6)

  • yarn add -D vitest@3 jsdom @testing-library/dom @testing-library/react @testing-library/jest-dom. Vitest pinned to 3 — yarn 1's peer resolution breaks on vitest@4's peer link to vite.
  • vitest.config.mts (separate from vite.config.mjs because the test runner shouldn't pull in the renderer SDK aliases).
  • tests/setup.ts imports @testing-library/jest-dom/vitest for custom matchers.
  • tests/WiredCreatorTools.helpers.test.ts (18 cases) covers the pure helpers extracted earlier — createEmptyMonitorSnapshot, formatMonitorLatestOccurrence (5 time-bucket branches), etc.

22a44d1 — Added useNitroEventState / useMessageEventState hooks (proposal #1)

The "derived state from a single event" pattern. Replaces the two-step useState + useNitroEvent(e => setState(...)) with a single call:

const foo = useNitroEventState(SomeEvent, e => e.payload, initial);
const data = useMessageEventState(SomeParser, e => e.getParser()?.field ?? null, null);

The selector is held in a useLayoutEffect-refreshed ref so the listener stays registered across renders.

bb1238a — Added useExternalSnapshot + useNitroEventReducer + useMessageEventReducer companion hooks

Pattern #1 family extended:

  • useExternalSnapshot for subscribing to renderer-side snapshot getters via useSyncExternalStore.
  • useNitroEventReducer / useMessageEventReducer for stateful event accumulation (reducer-style).

Other Phase 2 commits

  • 9d2e4a7 — Expanded Vitest coverage on the pure helpers in src/api/{utils,wired}.
  • 388fb8e — Migrated CatalogLayoutRoomAdsView's room-ad fetch to useNitroQuery.
  • bf84a0cuseNitroQuery extended with an accept() predicate; two mod-tools chatlog views migrated.
  • bb28d25 — Vitest +16 cases on ColorUtils, FixedSizeStack, LocalizeFormattedNumber.
  • dbafc97 — Dropped unused login dialogs (dead code) + Vitest coverage on FriendlyTime.
  • f75762a — Added CLAUDE.md + refreshed docs/ARCHITECTURE.md.
  • 559d860 — Pilot: moved InfoStand event listeners to useAvatarInfoWidget owner.
  • 8b7bedf — Pilot: extracted useInventoryFurni reducers to a pure module.
  • b1729d8 — Vitest: covered dedupeBadges with 6 cases.
  • f1af6fb — Docs: ARCHITECTURE pattern #1 — companions implemented, pilots adopted.
  • 8e4544c — Migrated catalog/giftConfiguration to useNitroQuery.

Phase 3: Hook taxonomy and god-hook splits

The god-hook decomposition pattern was applied across many of the project's central hooks.

Three-file flat-layout splits (state + actions + deprecated shim, all flat in the hooks barrel directory):

  • 0ae371euseFurniChooserWidget split.
  • 85fc827useUserChooserWidget split.
  • f3442f8useFriendRequestWidget split.
  • 7218285usePollWidget split into subscriptions + actions (proposal #4).
  • 419de09 — Hoisted usePollSubscriptions to RoomWidgetsView; dropped the side effect from usePollWidget.
  • a4c9dd8useChatInputWidget split.

useBetween-based singleton-filter splits for hooks shared by many consumers:

  • e1f5df6useWiredTools split into state + actions via useBetween singleton.
  • eeb9cc6useTranslation split via useBetween singleton.
  • 5344eafuseNotification split via useBetween singleton.
  • 9f3cd9buseFriends split via useBetween singleton.

Catalog three-way split (the most extensive decomposition)

  • fd3ef78 — Extracted pure helpers from useCatalog (buildCatalogNodeTree, findNodeById/findNodeByName, getNodesByOfferIdFromMap, getOfferProductKeys, normalizeCatalogType, resolveBuilderFurniPlaceableStatus) into src/hooks/catalog/useCatalog.helpers.ts. +34 Vitest cases on the pure helpers.
  • 59d6c4c — Three-way singleton-filter split: useCatalogData / useCatalogUiState / useCatalogActions. First 3 consumer migrations.
  • 0f9fa12 — Migrated remaining 36 useCatalog() consumers to the three filters. Deprecated useCatalog shim removed. Every consumer now subscribes only to the slice it actually reads, which restores React Compiler memoization and stops catalog-wide re-renders on unrelated key changes.

useNitroQuery adoption widening

  • 2d9785euseUserGroups: consolidated 4 dedup'd CatalogGroupsComposer call sites.
  • 2a5b9a4useClubOffers: per-windowId TanStack query for HC offer pages.
  • 3947781useSellablePetPalette(breed): per-breed TanStack query for pet picker.
  • 9a807bfuseMarketplaceConfiguration: lifted the marketplace config self-fetch.
  • 7b06229useClubGifts + useNitroEventInvalidator: closed the catalogOptions bag (composer/parser request-response with server-driven invalidation).
  • 8b79233 — Extracted useCatalogFavorites pure helpers + 16 Vitest cases.

Phase 4: WiredCreatorTools extraction and Zustand hoists

The WiredCreatorTools panel was the largest single component in the codebase. Phase 4 took it apart progressively.

File extractions

  • 5d8717d — Split WiredCreatorToolsView: extracted types/constants/helpers into 3 sibling files (WiredCreatorTools.types.ts, WiredCreatorTools.constants.ts, WiredCreatorTools.helpers.ts).
  • 23fc302 — Extracted Variables tab JSX into WiredVariablesTabView component.
  • d7d9a7e — Extracted Inspection tab JSX into WiredInspectionTabView component.
  • bb09a56 — Extracted Monitor tab JSX into WiredMonitorTabView + dropped dead overlays.

Progressive Zustand hoists (each its own commit for revertability)

  • c16ac1dUI flags hoisted to useWiredCreatorToolsUiStore: 14 pure UI flags (isVisible, activeTab, inspectionType, variablesType, modal/popover opens, monitor and variable-manage filters/sort/page). Setters accept value-or-updater. WiredInspectionTabView and WiredVariablesTabView drop 6 props.
  • eb8d879 — Docs follow-up (CLAUDE: store adoption + test count bump).
  • 82bccd4monitorSnapshot hoisted + polling reset. Server-pushed stats now survive panel close/reopen.
  • 7758af7 — Docs: vitest count bump after monitorSnapshot cases.
  • 8182e06Selection hoisted (selectedFurni, selectedFurniLiveState, selectedUser, selectedUserLiveState, selectedUserActionVersion). Listeners (useObjectSelectedEvent, per-kind useMessageEvent handlers) stay in the component (need React lifecycle) and call store actions. Live-state setters keep the Updater<T> shape so the ~10 previousValue => ... call sites stay verbatim.
  • 50fd908 — Docs: vitest count bump.
  • 0fc32a1Variable-highlight hoisted (isVariableHighlightActive toggle + variableHighlightOverlays screen-coords array). WiredVariablesTabView drops two more props. The two screen-coords recompute effects stay in the component (need React lifecycle for WiredSelectionVisualizer install/teardown). variableHighlightObjectsRef stays as useRef (refs don't belong in Zustand).
  • c1aafff — Docs: vitest count bump.
  • 181ca09Inline editor hoisted (editingVariable / editingValue / editingManagedHolderVariableId / editingManagedHolderValue). WiredInspectionTabView drops three more props. shouldPauseVariableSnapshotRefresh still reads from the same store-backed values.
  • 438b47d — Docs: vitest count bump to 193/193.

Final picker hoists (2026-05-18 session — three commits closing the roadmap)

  • ba77806Variable-key records hoisted (selectedInspectionVariableKeys, selectedVariableKeys). Setter shape Updater<Record<...Type, string>> because all writers used prev => ({ ...prev, [type]: key }). Empty defaults — the existing definition-sync effect at WiredCreatorToolsView.tsx:1543-1574 populates them on first render. +4 test cases.
  • 8894fccInspection give pickers hoisted (inspectionGiveVariableItemId, inspectionGiveValue). Plain typed setters, sentinel pair 0/'0'. +2 test cases.
  • 1c2d8daManaged-holder give picker chain hoisted (selectedManagedVariableEntry, selectedManagedHolderVariableId, managedGiveVariableItemId, managedGiveValue). Cascade reset effects at 2265-2307 stay in the component. +4 test cases.

Roadmap result: every useState left in WiredCreatorToolsView.tsx after 1c2d8da is genuinely transient (keepSelected, globalClock, roomEnteredAt, selectedMonitorErrorType, selectedMonitorLogDetails) — none would benefit from store-backed persistence.

Phase 5: Typecheck cleanup (Pixi v8, TS 6, framer-motion)

A separate sweep aimed at getting yarn typecheck to 0 errors (initial state was ~50+ errors carried over from prior version bumps).

  • b5eeb68 — Typed framer-motion variants as Variants — killed 33 tsgo errors.
  • 96b61ff — Fixed 4 typecheck errors in createNitroQuery.
  • feba672 — Sweep: union expansions + React 19 JSX + extra arg.
  • 1083b2e — Typed useFurniChooserState builders + dropped dead getUserData guard.
  • a39aa37 — React 19: useRef<T>() → useRef<T>(null) across 15 sites (React 19 made this required at the type level).
  • f57266a — Updated 3 IGetImageListener.imageReady call sites to Pixi v8 single-arg signature.
  • a8065f6 — Added optional clone() to IPurchasableOffer.
  • 71a1586 — Stripped dead server-sync from UiSettingsContext + re-exported ui-settings.
  • 0192952 — Sweep: 11 fixes across 9 files.
  • 2a9a5dd — Added react-colorful dep for InterfaceColorTabView.
  • f09bb7e — Pixi v8 alignment in 2 room-widget helpers.
  • 0c43377 — Dropped dead await success on fire-and-forget catalog-admin actions.
  • 68de96c — Last-mile typecheck sweep: 3 small bugs.

Phase 6: Error boundaries and logic-bug fixes

WidgetErrorBoundary framework

src/common/error-boundary/WidgetErrorBoundary.tsx wraps any in-room widget tree so a crash degrades gracefully (logs to NitroLogger, falls back to null). Applied at RoomWidgetsView as an umbrella initially:

  • ab93113 — Wrapped each room + furniture widget (13 room widgets + 20 furniture widgets) in its own WidgetErrorBoundary so a crash in one widget no longer takes down its siblings.

Logic-bug fixes documented during the modernization

  • 81656e7 — Fixed two logic bugs found while refactoring + documented the open ones in docs/ARCHITECTURE.md.
  • 9d10e52Fix(MainView): collapsed CREATED/ENDED listeners into a session-aware reducer. The previous two-effect pattern had a race: a RoomSessionEvent.ENDED for a stale session could clear the current session's state if it arrived after CREATED. The reducer now compares session tokens.
  • 97c9717Fix(layout-image): guarded async image fetch with a requestIdRef. Resolved a race where props changed twice in quick succession could land the second fetch's result first, then the first's, overwriting valid data with stale.
  • b01f09cfix: null-check the set type before reading .paletteID in avatar editor.

Phase 7: Test infrastructure evolution

  • 6793de2 — Vitest setup + 22 initial smoke tests (covered in Phase 2).
  • bb28d25 — +16 cases on ColorUtils / FixedSizeStack / LocalizeFormattedNumber.
  • dbafc97 — Vitest coverage on FriendlyTime; dropped dead login dialogs.
  • b1729d8dedupeBadges with 6 cases.
  • 3c732f1avatarInfo reducers with 14 cases.
  • c401839Renderer-SDK mock layer at tests/mocks/renderer-mock.ts (later flattened to src/nitro-renderer.mock.ts). Stubs:
    • Explicit behavioral stubs for symbols tests actually exercise (NitroLogger, GetEventDispatcher, mockEventDispatcher helpers, RoomSessionDoorbellEvent).
    • String-keyed Proxy enums for NitroEventType, RoomObjectCategory, AvatarFigurePartType.
    • Lightweight class StubClass {} placeholders for ~30 Pixi and gameplay classes the src/api/* barrel touches.
    • Singleton getters returning chainable Proxies.
    • First 2 component-/hook-level pilots: WidgetErrorBoundary (4 tests) + useDoorbellState (7 tests).
  • fd3ef78 — Catalog pure-helper extraction + 34 Vitest cases.
  • 8b79233useCatalogFavorites pure helpers + 16 Vitest cases.
  • 8b4308aTests co-located under src/ — every *.test.ts(x) moved next to its subject.
  • 803de20 — Flattened renderer mock to src/nitro-renderer.mock.ts (dropped __mocks__/).

Phase 8: CI pipeline

  • 8844cc1ci: ran typecheck + Vitest on every push to main / feat/** and on every PR. Workflow at .github/workflows/ci.yml.
  • 53fc5f0ci: created renderer symlink AFTER yarn install, not before (yarn install would otherwise nuke the symlink).
  • 5d7a20aci: used absolute symlink target + checked out feat/react19-event-bus on the renderer fork.
  • cb7502fci: opted JavaScript actions into Node.js 24.

Phase 9: Upstream cherry-picks (PR #126) and drive-by bugs

Mid-modernization, two upstream commits from duckietm/Nitro-V3 PR #126 were cherry-picked into the branch so the modernization branch would carry features still pending upstream merge:

  • 52b0c90 — Merge commit from PR #126 (merge of upstream into the local branch — at this stage upstream had not yet reached b2318b9).
  • 53b0c90 53f41cd 2053c8e — Fix wear badge popup + UserAccountSettingsView (reset password / email / username under user settings).
  • 3a7c9ba — Same wear-badge popup fix (rebased version).
  • 9ef6983 — Post-cherry-pick: restored useEffectEvent wrapper + fixed configuration import (the cherry-pick's mechanical drift broke a few wires).
  • 622d73c — Docs: reflected PR #126 cherry-pick + boot/asset infrastructure.

Boot/asset infrastructure went in here too:

  • 45620cavite: actually split the renderer into its own chunk.
  • cd8951edev: served game assets via sirv plugin and pre-init configuration. The chokidar-on-177k-files problem on Windows: serving game assets through sirv middleware mounted on /nitro-assets and /swf reading from E:\Users\simol\Desktop\DEV\Nitro-Files\ bypasses chokidar entirely. The plugin also wires the same handler into configurePreviewServer. Same commit introduced await GetConfiguration().init() in src/bootstrap.ts before importing ./index — otherwise the first paint flooded with "Missing configuration key" warnings while components synchronously read keys against an empty store.
  • 35b8493vite: failed fast with a setup hint when the renderer SDK is missing.
  • 8e0bcce — Added yarn preview script for serving the production build.

Other Phase 9 commits

  • 7cf01b0 — Docs: refresh ARCHITECTURE + CLAUDE.
  • cc225bd — Docs: comprehensive refresh after React 19 modernization round.

Phase 10: Toolbar spam-toggle fix (PR #130 upstream)

4ab38d3toolbar: always-mount nav rows + drive show/hide via framer variants. Replaced the outer AnimatePresence wrapper around the four toolbar rows (desktop backplate, left-nav, right-nav, mobile-nav) with always-mounted motion.div elements driven by an isVisible-derived variant string ('visible' or 'hidden').

The bug it fixed: rapid clicks on the show/hide chevron previously left motion children in inconsistent intermediate states (stuck opacity 0, phantom scale 0.8) because AnimatePresence + Fragment + multiple keyed children breaks when enter/exit cycles overlap. With variants, framer-motion's spring solver picks up from the current animated value on each retarget, so spam-clicking just settles smoothly toward whichever target is current.

Refactor details:

  • containerVariants dropped its 'exit' state (now lives in 'hidden').
  • itemVariants dropped 'exit' as well.
  • New shellVariants for the backplate.
  • pointer-events animated per-variant ('auto' visible / 'none' hidden) instead of pinned via a Tailwind class, so hidden rows don't intercept clicks.
  • Wrapper variants computed inside the component because leftNavVariants.hidden depends on isInRoom (nav slides in from the side in-room, from the bottom otherwise).
  • Variant inheritance: outer wrapper drives 'visible'/'hidden'; inner container and items inherit via framer's variant propagation, so stagger runs in both directions without needing AnimatePresence.
  • Inner AnimatePresence around the Me popover stays (it has a single child, no spam-toggle risk).

The same fix opened upstream as PR #130 on duckietm/Nitro-V3 (branch simoleo89:fix/toolbar-spam-show-hide).

Phase 11: Full upstream sync (origin/Dev b2318b9)

After Phase 10, the local branch was 98 commits ahead of origin/Dev. Upstream had 10 commits the branch needed to absorb. Done in the 2026-05-18 session.

  • Tagged rollback: pre-upstream-merge-20260518 at 4ab38d3.
  • git merge --no-ff origin/Dev produced 6 conflicts in package.json, src/App.tsx, src/bootstrap.ts, src/components/login/LoginView.tsx, src/components/notification-center/views/bubble-layouts/NotificationBadgeReceivedBubbleView.tsx, vite.config.mjs, yarn.lock.
  • Resolution: kept modernized structure on the conflict files, surgically applied upstream intent where it added value (json5 dep, JSON5.parse fallback in bootstrap.ts, base config in vite.config.mjs). The persistAccessTokenFromPayload(payload) upstream fix in LoginView.tsx was already present in the modernized Form Actions SSO branch — no work needed.
  • Two user-settings views auto-merged because the local branch had already cherry-picked the same commit (cdf8d92 upstream = 2053c8e local byte-for-byte).
  • Five files came in upstream-only with no local divergence.
  • yarn.lock regenerated via git checkout --ours yarn.lock then yarn install.

Verification: typecheck clean, Vitest 193/193 (preserved baseline), yarn build green.

Commit 779a98c — merge commit. Followed by 3b35fa9 — CLAUDE.md refresh (TL;DR + wired-up table updated to reflect post-merge state with note about expected future conflict surface).

Phase 12: Snapshot pattern consumer-side wiring + first migrations

After Phase 11 closed the WiredCreatorTools roadmap, the renderer side had six snapshot getters (Session, RoomSession, IgnoredUsers, GroupBadges, RoomUserList, SoundVolumes) but nothing on the client consumed them — useExternalSnapshot existed as a useSyncExternalStore wrapper, no widget was wired up to a snapshot.

b2a86da — React-side consumer hooks for the renderer snapshot pattern

New file src/hooks/session/useSessionSnapshots.ts exposes eight thin hooks, each a useExternalSnapshot wrapper around the matching subscribe + getter pair:

Hook Returns
useUserDataSnapshot() Readonly<IUserDataSnapshot>
useActiveRoomSessionSnapshot() Readonly<IRoomSessionSnapshot> | null
useIgnoredUsersSnapshot() ReadonlyArray<string>
useIsUserIgnored(name) boolean (memoized)
useGroupBadgesSnapshot() ReadonlyMap<number, string>
useGroupBadge(groupId) string (memoized)
useVolumesSnapshot() Readonly<ISoundVolumesSnapshot>
useRoomUserListSnapshot() ReadonlyArray<IRoomUserData>

Two design subtleties documented inline:

  • useRoomUserListSnapshot subscribes to BOTH ROOM_USER_LIST_UPDATED (for join/leave/update inside the active session) AND ROOM_SESSION_UPDATED (because the underlying userDataManager reference flips when the active room changes). A 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 fires when the underlying snapshot reference flips, not on unrelated useExternalSnapshot wake-ups.

71a0eee — Migrate useSessionInfo to useUserDataSnapshot

First consumer migration. The old useSessionInfo carried three useState mirrors of session data (userFigure / userRespectRemaining / petRespectRemaining) driven by useMessageEvent<UserInfoEvent> + useMessageEvent<FigureUpdateEvent> + manual setUser… after giveRespect. Replaced with a single useUserDataSnapshot() read. SessionDataManager already invalidates its snapshot on every state change that mattered to the old hook (UserInfoEvent handler, FigureUpdateEvent listener, giveRespect / givePetRespect) — so the snapshot is a strict superset of the manual mirror.

Net result: 3 useState declarations + 2 useMessageEvent subscriptions removed; respectUser / respectPet become trivial pass-throughs because the manager's invalidate dispatches the event for us. chatStyleId stays on useState (driven by UserSettingsEvent, not in the snapshot). The deprecated userInfo: UserInfoDataParser field is dropped from the return shape — no in-tree consumer reads it.

36addbe — Reactive Ignore/Unignore menu entry via useIsUserIgnored

The Ignore ↔ Unignore context-menu entry in AvatarInfoWidgetAvatarView was driven by avatarInfo.isIgnored — a boolean captured by AvatarInfoUtilities once, at the time the avatar was clicked. If the user got ignored / unignored while the popup was already open (e.g. via the friends panel, or because a server push flipped the state), the menu kept showing the stale option and clicking it would no-op (or worse, double-ignore).

Switched the menu items to useIsUserIgnored(avatarInfo.name) — the reactive hook backed by IgnoredUsersManager.getIgnoredUsersSnapshot() + NitroEventType.IGNORED_USERS_UPDATED. The menu now flips automatically the moment the ignore list changes, without re-opening.

02a396d — docs(CLAUDE.md): refresh stale sections

Aligning Nitro-V3/CLAUDE.md with the current branch state:

  • Adopted table: new row for the snapshot consumer hooks (pilots on useSessionInfo + AvatarInfoWidgetAvatarView); Vitest count bumped 193 → 203; Zustand row expanded to note the WiredCreatorTools panel-lifecycle hoist roadmap is fully closed.
  • Not yet table: dropped the obsolete "hoist Wired Creator Tools derived state" row; added a new row for migrating remaining session-data mirrors.
  • New "Patterns to use" entry at the top documenting the 8-hook useSessionSnapshots menu.
  • "Known open logic bugs" replaced with a "no open bugs" entry (both previously-open races are closed in 9d10e52 and 97c9717).

3. Nitro_Render_V3 renderer — branch story

Phase 1: v2.1.0 React-friendly API additions

87cf478feat(events,session): add React-friendly subscribe APIs and snapshot getters. The single foundational commit that gave the branch its name. Backwards-compatible additions needed to consume the renderer from React 19 hooks (useSyncExternalStore, use(), TanStack Query):

  • EventDispatcher.subscribe(type, cb): () => void — unsubscriber-returning wrapper matching the useSyncExternalStore subscribe signature. Legacy addEventListener / removeEventListener still work.
  • CommunicationManager.subscribeMessage(eventCtor, handler): () => void — packet-stream equivalent.
  • SessionDataManager.getUserDataSnapshot(): Readonly<IUserDataSnapshot> — referentially-stable read-only view invalidated through the new SESSION_DATA_UPDATED event.
  • RoomSessionManager.getActiveRoomSessionSnapshot(): Readonly<IRoomSessionSnapshot> | null — same pattern, invalidated through ROOM_SESSION_UPDATED.

The interface contracts:

  • packages/api/src/nitro/session/IUserDataSnapshot.ts
  • packages/api/src/nitro/session/IRoomSessionSnapshot.ts

Bumped renderer to 2.1.0.

Phase 2: TypeScript 6 + tsgo migration

  • c7a5aea — Bumped TypeScript 5.8 → 6.0. Added @typescript/native-preview (tsgo, the TS 7 preview compiler) as yarn compile:fast (~7× faster: 2.5s vs 17.6s). Tsconfig cleanup ahead of TypeScript 7 deprecations:
    • Removed baseUrl (unused).
    • Removed downlevelIteration (target ES2022 makes it a no-op).
    • moduleResolution: "Node""bundler".
    • Compile errors: 28 → 29 (TS 6's tightened lib types flagged two pre-existing crypto calls).
  • ddb7222 — Bumped TypeScript pins to ^6.0.3 across all 12 workspaces + thumbmarkjs 1.9. Added CLAUDE.md to the renderer.
  • e82d3e0chore(types): augmented ImportMeta with glob signature.

Phase 3: API interface alignments (IRoomSession)

afb5f33fix(api): IRoomSession.password + sendBackgroundMessage + optional chatColour. The IRoomSession interface was missing three things that always existed on the RoomSession implementation class:

  • password: string — the room session's join password (used by the reconnect flow in RoomSessionManager).
  • sendBackgroundMessage(backgroundImage, backgroundStand, backgroundOverlay, backgroundCard?) — sends the profile-background composer (used by the React client's BackgroundsView).
  • sendChatMessage / sendShoutMessage chatColour parameter relaxed to optional. The implementation already accepted undefined; every historical call site in the React client passes only 2 args.

Net renderer typecheck: 26 → 23. Dropped 7 errors on the consumer side after the workspace link picked up the change.

Phase 4: TS 5.7+ ArrayBuffer drift fixes

c37171aTS 5.7+ ArrayBuffer drift: cast where ArrayBufferLike leaked. The renderer never uses SharedArrayBuffer, so the type-level narrowings are safe. Sites cast:

  • BinaryReader / BinaryWritergetBuffer() / toArrayBuffer().
  • WsSessionCrypto.randomNonce().
  • ArrayBufferToBase64.

Phase 5: Pixi v8 alignment

5ea3201Align with Pixi v8: Filter[] union, WebGLRenderer narrow, ImageLike. Four sites where Pixi v8's stricter typing tripped tsgo:

  • AvatarImage: container.filters is readonly Filter[] | null in v8. Old fallback branch else container.filters = [container.filters, …] tried to treat a readonly array as a single Filter. Collapsed to the array-spread path which covers both undefined and non-empty cases.
  • FurnitureBadgeDisplayVisualization.updateSprite() had a 4-arg override (sprite, asset, scale, layerId) of the parent's 2-arg (scale, layerId) signature. Refactored to fetch the sprite via this.getSprite(layerId) inside the override body.
  • ExtendedSprite: renderer.gl / glRenderTarget.resolveTargetFramebuffer exist only on WebGLRenderer / GlRenderTarget. The runtime check renderer.type === RendererType.WEBGL guarantees this; cast at the boundary.
  • TextureUtils.generateImage: Pixi v8's Extractor.image() returns the union ImageLike (HTMLCanvasElement | HTMLImageElement); the public signature promises HTMLImageElement. Cast at return.

Phase 6: Composer/parser alignment with Arcturus

  • b42f989RoomEnterComposer: optional spawnX/spawnY for reconnect. Arcturus' RequestRoomLoadEvent reads the two extra ints only when the inbound packet has 8+ bytes remaining, so the renderer can send 2-arg or 4-arg payloads against the same header. The client already called the 4-arg variant in two places inside RoomSession/RoomSessionManager — the composer signature was lagging behind.
  • 0fc38a1 — Fixed self-referential ConstructorParameters in two Wired composers (WiredRoomSettingsRequestComposer, WiredUserVariablesRequestComposer — empty-tuple composers needed explicit getMessageArray(): [] annotation).
  • 999b818 — Fixed PetBreedingMessageParser.bytesAvailable < 12 (bytesAvailable is a boolean, not a byte count — the old code compared it against 12 which TS 6 caught).
  • ef6c661Renderer: surface allowUnderpass on RoomSettingsData + composer. Arcturus' RoomSettingsComposer appends an extra int at the end of the payload — room.isAllowUnderpass() ? 1 : 0 — and RoomSettingsSaveEvent optionally reads back a boolean at the end (if(bytesAvailable > 0)). The renderer side never modeled this trailing field. Added the field + parser guard + optional trailing composer arg. Net client tsgo error count: 3 → 0 on the NavigatorRoomSettings cluster.
  • 22d4e5b — SocketConnection parser cast + RoomChatHandler arg-order fix.
  • f7a5897 — Renderer: aligned NitroConfig Window declaration with the client + fixed glob .default access.

Phase 7: Dead code removal and small fixes

  • 08d1efaDrop dead sendWhisperGroupMessage. IRoomSession.sendWhisperGroupMessage(userId) referenced a ChatWhisperGroupComposer that never existed in the codebase and had zero call sites in the React client. Both the interface declaration and the broken impl removed. The real whisper path is RoomUnitChatWhisperComposer(recipientName, message, styleId) — unchanged.
  • 5f5ba2f — Docs: documented recent feat/react19-event-bus additions in CLAUDE.md.

Phase 8: Upstream sync (origin/main)

Done in the 2026-05-18 session. Zero file intersection between the 15 local commits and the 1 non-merge upstream commit (b6a26fb — small landscape-offset fix in RoomPlane.ts).

  • Tagged rollback: pre-upstream-merge-20260518 at 5f5ba2f.
  • git merge --no-ff origin/main auto-completed with no conflict prompts.
  • Commit 820f791.
  • Verification: yarn compile:fast clean, Vitest 104/104.

Phase 9: Snapshot pattern extensions

Five commits in the 2026-05-18 session extending the v2.1.0 snapshot pattern to four new state holders.

98662e7 — BinaryReader / BinaryWriter round-trip Vitest coverage (23 cases)

Added comprehensive round-trip tests under packages/utils/src/__tests__/BinaryReader.test.ts:

  • byte / short / int round-trips, including signed-edge values (int8 -1 from 0xFF, int16 / int32 boundaries)
  • big-endian wire-order assertions on writeShort / writeInt (matches Arcturus's DataInputStream)
  • string round-trip with length prefix + bare (includeLength=false) + UTF-8 multibyte byte count + empty-string edge
  • writeBytes for both number[] and ArrayBuffer payloads
  • readBytes slice returns an independent reader whose position is decoupled from the outer reader
  • remaining() decrements correctly across mixed-size reads
  • readFloat / readDouble decode IEEE-754 big-endian values (the writer has no float/double counterparts — buffer built via DataView for these cases)
  • writer position getter + explicit setter (caller-managed reposition)
  • two independent writers concatenate cleanly into a single reader

Note: retrospectively deprioritized — mid-session the user redirected with "anche se renderer devi ragione la ui, niente tests". Pure Vitest coverage growth on the renderer is no longer considered modernization progress; UI-affecting changes (rendering, API surface, perf) are preferred. The test commit was kept (no regression, useful safety net for any future BinaryReader changes) but flagged as not the kind of work to repeat by default. A feedback memory entry (feedback_renderer_ui_over_tests.md) captures this preference.

a599e0c — feat(session): snapshot getters for IgnoredUsersManager + GroupInformationManager

IgnoredUsersManager.getIgnoredUsersSnapshot(): ReadonlyArray<string> — wrapped the existing _ignoredUsers: string[] with a snapshot getter. Invalidation hooked into:

  • onIgnoredUsersEvent (initial server-side list fetch).
  • addUserToIgnoreList (ignore action result code 1 and 2).
  • removeUserFromIgnoreList (unignore action result code 3).
  • After the special _ignoredUsers.shift() operation that runs alongside addUserToIgnoreList for result code 2 (queue truncation) — added explicit invalidateIgnoredUsersSnapshot() after the shift so the dispatched event fires only once the truncation is complete.

GroupInformationManager.getGroupBadgesSnapshot(): ReadonlyMap<number, string> — wrapped _groupBadges: Map<number, string> with a snapshot getter. The onGroupBadgesEvent handler now compares each incoming badge against the cached value and only flips a didChange flag if an entry is new or actually changed. Invalidation fires only when didChange === true.

New NitroEventType members: IGNORED_USERS_UPDATED, GROUP_BADGES_UPDATED.

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

UserDataManager.getRoomUserListSnapshot(): ReadonlyArray<IRoomUserData> — the biggest snapshot in terms of invalidation surface. 11 mutation paths all wired: updateUserData, removeUserData, updateFigure, updateName, updateMotto, updateNickIcon, updateCustomization, updateBackground, updateAchievementScore, updatePetLevel, updatePetBreedingStatus.

Design decision — no deep-clone: the inner IRoomUserData objects keep the existing in-place mutation semantics. Deep-cloning a snapshot of 30+ avatars on every server-pushed status event would defeat the snapshot's purpose. TSDoc on the interface explicitly documents that consumers should treat each entry as a snapshot-at-time-of-read and not retain references across invalidations.

Drive-by cleanup: updatePetLevel previously used an inline conditional; rewritten to use the explicit if(!userData) return; guard pattern shared by the surrounding methods.

New NitroEventType member: ROOM_USER_LIST_UPDATED.

892d16b — feat(sound): snapshot getter + volume-update event on SoundManager + bug fix

SoundManager.getVolumesSnapshot(): Readonly<ISoundVolumesSnapshot> — new ISoundVolumesSnapshot { system, furni, trax } interface. New systemVolume / furniVolume getters for parity with the pre-existing traxVolume.

Drive-by bug fix — volume diff comparison was always wrong. The previous onEvent(SETTINGS_UPDATED) handler compared castedEvent.volumeFurni (percent, e.g. 75) against this._volumeFurni (the already-divided fraction, e.g. 0.75). The check almost never reported "unchanged" for any real settings push. Both updateFurniSamplesVolume and _musicController.updateVolume were being called on every settings push regardless of whether the volume actually changed.

Fix: divide first into local variables, compare divided values against the stored fractions, then write. Also tracks volumeSystemUpdated for the new snapshot's invalidation event.

New NitroEventType member: SOUND_VOLUMES_UPDATED.

d740f83 — refactor(parsers): flatten nested bytesAvailable guards

Two parsers had nested if(wrapper.bytesAvailable) chains making each new optional trailing block sit one extra indent deeper than the previous:

  • UserProfileParser — 4 optional trailing tiers (background/stand/overlay 3 ints, cardBackgroundId 1 int, nickIcon 1 string, prefix decoration set 6 strings). Previously 4 levels of nested if with an inline ternary mid-block for cardBackgroundId. Refactored to a flat early-return chain.
  • GetGuestRoomResultMessageParser — 2 optional trailing tiers (hotelTimeZoneId + hotelCurrentTimeMs 2 strings, roomItemLimit 1 int). Previously 2 levels of nested if. Refactored to flat early-return.

Both files now follow the canonical pattern:

if(!wrapper.bytesAvailable) return true;
// block N reads
if(!wrapper.bytesAvailable) return true;
// block N+1 reads

Each block documented inline so the contract is obvious without cross-referencing Arcturus. Adding tier N+1 is now purely additive — no re-indentation of existing blocks.

An audit across all 29 parsers using bytesAvailable found exactly these two files with nested-guard chains. All other parsers either use a single optional trailing field or already used the flat pattern (RoomUnitInfoParser was the reference).

28c552f — docs(CLAUDE.md): document new snapshot getters + flat bytesAvailable pattern

Replaced the two-getter SessionData / RoomSession snapshot description in Nitro_Render_V3/CLAUDE.md with a six-row table covering every snapshot currently exposed:

Manager Getter Invalidation event
SessionDataManager getUserDataSnapshot(): Readonly<IUserDataSnapshot> SESSION_DATA_UPDATED
RoomSessionManager getActiveRoomSessionSnapshot(): Readonly<IRoomSessionSnapshot> | null ROOM_SESSION_UPDATED
IgnoredUsersManager getIgnoredUsersSnapshot(): ReadonlyArray<string> IGNORED_USERS_UPDATED
GroupInformationManager getGroupBadgesSnapshot(): ReadonlyMap<number, string> GROUP_BADGES_UPDATED
UserDataManager getRoomUserListSnapshot(): ReadonlyArray<IRoomUserData> ROOM_USER_LIST_UPDATED
SoundManager getVolumesSnapshot(): Readonly<ISoundVolumesSnapshot> SOUND_VOLUMES_UPDATED

Plus a 3-step checklist for adding new ones and a dedicated section on the flat bytesAvailable early-return pattern as the canonical shape for optional-trailing-field parsers.


4. Arcturus emulator — upstream pull

In the same session, the Java emulator was brought from e6093f9 (v4.1.14) to efb4997 (v4.1.16) via a fast-forward pull from origin/main (duckietm). Zero local divergence existed; the FF pull absorbed 8 upstream commits:

  • Version bumps to 4.1.15 and 4.1.16.
  • Database migrations 000-019 reorganized under Database Updates/Own_Database_RunFirst/, with new files 010_Wired_Update.sql, 018_Last_Username_Change.sql, and a renamed 019_custom_nick_login_tokens_wired_message.sql.
  • New auth-related Java classes: AccountChangeEndpoints, AccountCheckEndpoints, AuthHttpUtil, CorsOriginGate, RegistrationSupport, SessionEndpoints, StaticContentEndpoints.
  • ChangeNameCommand.java removed (replaced by API endpoints).
  • Updates to AboutCommand, CommandHandler, GuildManager, multiple guild event handlers, WebSocketChannelInitializer.
  • Default DB file renamed FullDB.sqlFullDatabase.sql.

Local working-tree modifications (customized config.ini.example shorter inline comments; untracked Habbo-4.1.15-jar-with-dependencies.jar and emulator.cmd) survived the pull intact. No push performed (local tracks origin/main directly).

Rollback tag: pre-upstream-pull-20260518 at e6093f9.


5. Documentation evolution (CLAUDE.md / ARCHITECTURE.md)

Both branches maintained substantial in-tree documentation throughout their lifetime.

Nitro-V3

docs/ARCHITECTURE.md (introduced in 48d62c5) — Living long-form document describing where the project stands, the five structural proposals, and the next-PR recommended order. Updated across multiple commits as proposals landed:

  • 0755285 — recorded the feature-folder reversion.
  • 7218285 — proposal #4 landed (poll-widget split).
  • f1af6fb — pattern #1 companions implemented, pilots adopted.
  • 7cf01b0, cc225bd, 622d73c — comprehensive refresh sweeps.

Nitro-V3/CLAUDE.md (added in f75762a) — Project context summarized for Claude Code sessions. Refreshed across the modernization:

  • eb8d879, 7758af7, 50fd908, c1aafff, 438b47d — vitest count bumps after each hoist.
  • 3b35fa9 — post-upstream-merge refresh.

Nitro_Render_V3

Nitro_Render_V3/CLAUDE.md (added in ddb7222) — Renderer context for Claude Code sessions. Refreshed:

  • 5f5ba2f — documented feat/react19-event-bus additions.
  • 28c552f — documented new snapshot getters + flat bytesAvailable pattern.

6. Full commit index

Nitro-V3 — feat/react19-modernization (109 commits, baseline ae17619)

Phase 1: React 19 baseline

SHA Subject
cdf8d92 🆕 Added Reset password / Email and change username in user settings (upstream)
a1bee1d React 19 modernization: forwardRef removal, Compiler, ErrorBoundary, Suspense, native <script>
1b1e0c1 React 19 Phase 3: login/forgot/register forms → useActionState + useFormStatus
535fa71 ESLint --fix: auto-fix brace-style, indent, semi, no-trailing-spaces
25d51af Enable <StrictMode> + make App.tsx renderer init idempotent
13dc483 Bump ecosystem dependencies (minor/patch)
5697d16 Fix rules-of-hooks violation in InfiniteGrid
6c9f414 Apply useEffectEvent (React 19.2) to TurnstileWidget callbacks
f18c917 Add TypeScript 7 (tsgo) as fast type-checker alongside TS 6
d382635 Phase A: clear all react-hooks/exhaustive-deps warnings via useEffectEvent or hoisting
39eb2c6 Phase C (targeted): clear 4 set-state-in-effect violations on safe candidates

Phase 2: Infrastructure pillars

SHA Subject
5d8717d Split WiredCreatorToolsView: extract types/constants/helpers into 3 sibling files
22a44d1 Add useNitroEventState / useMessageEventState hooks (proposal #1)
48d62c5 Architecture refactor: docs + 5 pilot implementations + error boundary
81656e7 Fix two logic bugs found while refactoring + document the open ones
0755285 Revert feature-folder migration; keep classic src/components + src/hooks layout
34b1b56 Enable React Query (proposal #2) + first real-data pilot on OfferView
fd1835c Enable Zustand (proposal #5) + convert isCreatingRoom singleton
6793de2 Set up Vitest + 22 smoke tests on pure modules (proposal #6)
7218285 Split usePollWidget into subscriptions + actions (proposal #4) + doc update
419de09 Hoist usePollSubscriptions to RoomWidgetsView; drop the side effect from usePollWidget
9d2e4a7 Expand Vitest coverage on the pure helpers in src/api/{utils,wired}
388fb8e Migrate CatalogLayoutRoomAdsView's room-ad fetch to useNitroQuery
bf84a0c useNitroQuery: add accept() predicate; migrate two mod-tools chatlog views
bb28d25 Vitest: +16 cases on ColorUtils, FixedSizeStack, LocalizeFormattedNumber
dbafc97 Drop unused login dialogs (dead code) + Vitest coverage on FriendlyTime
f75762a Add CLAUDE.md + refresh docs/ARCHITECTURE.md to current state
bb1238a Add useExternalSnapshot + useNitroEventReducer + useMessageEventReducer hooks

Phase 3: God-hook splits + tab extractions

SHA Subject
559d860 Pilot: move InfoStand event listeners to useAvatarInfoWidget owner
8b7bedf Pilot: extract useInventoryFurni reducers to a pure module
b1729d8 Vitest: cover dedupeBadges with 6 cases
f1af6fb docs: ARCHITECTURE pattern #1 — companions implemented, pilots adopted
8e4544c Migrate catalog giftConfiguration to useNitroQuery
23fc302 Extract Variables tab JSX into WiredVariablesTabView component
d7d9a7e Extract Inspection tab JSX into WiredInspectionTabView component
bb09a56 Extract Monitor tab JSX into WiredMonitorTabView + drop dead overlays
0ae371e Split useFurniChooserWidget into state + actions (flat hooks layout)
85fc827 Split useUserChooserWidget into state + actions (flat hooks layout)
f3442f8 Split useFriendRequestWidget into state + actions (flat hooks layout)
a4c9dd8 Split useChatInputWidget into state + actions (flat hooks layout)
e1f5df6 Split useWiredTools into state + actions via useBetween singleton
eeb9cc6 Split useTranslation into state + actions via useBetween singleton
5344eaf Split useNotification into state + actions via useBetween singleton
9f3cd9b Split useFriends into state + actions via useBetween singleton

Phase 4: useNitroQuery widening + catalog split

SHA Subject
2d9785e useUserGroups: consolidate 4 dedup'd CatalogGroupsComposer call sites
2a5b9a4 useClubOffers: per-windowId TanStack query for HC offer pages
3947781 useSellablePetPalette(breed): per-breed TanStack query for pet picker
9a807bf useMarketplaceConfiguration: lift the marketplace config self-fetch
7b06229 useClubGifts + useNitroEventInvalidator: close the catalogOptions bag
8b79233 Extract useCatalogFavorites pure helpers + 16 Vitest cases
fd3ef78 catalog: extract pure helpers + 34 cases, consume them from useCatalog
59d6c4c catalog: three-way singleton-filter split + first 3 consumer migrations
0f9fa12 catalog: migrate remaining 36 useCatalog() consumers to the three filters

Phase 5: Typecheck cleanup

SHA Subject
b5eeb68 Type framer-motion variants as Variants — kill 33 tsgo errors
96b61ff Fix 4 typecheck errors in createNitroQuery
feba672 Sweep small typecheck nits: union expansions + React 19 JSX + extra arg
1083b2e Type useFurniChooserState builders + drop dead getUserData guard
a39aa37 React 19: useRef() -> useRef(null) across 15 sites
f57266a Update 3 IGetImageListener.imageReady call sites to v8 single-arg signature
a8065f6 Add optional clone() to IPurchasableOffer
71a1586 Strip dead server-sync from UiSettingsContext + re-export ui-settings
0192952 Sweep targeted typecheck errors: 11 fixes across 9 files
2a9a5dd Add react-colorful dep for InterfaceColorTabView
f09bb7e Pixi v8 alignment in 2 room-widget helpers
0c43377 Drop dead 'await success' on fire-and-forget catalog-admin actions
68de96c Last-mile typecheck sweep: 3 small bugs

Phase 6: Logic-bug fixes + WidgetErrorBoundary

SHA Subject
9d10e52 fix(MainView): collapse CREATED/ENDED listeners into a session-aware reducer
97c9717 fix(layout-image): guard async image fetch with a request-id ref
ab93113 widgets: wrap each room + furniture widget in its own WidgetErrorBoundary
b01f09c fix: null-check the set type before reading .paletteID in avatar editor

Phase 7: Test infrastructure evolution

SHA Subject
c401839 tests: add renderer-SDK mock layer + first 2 component-/hook-level pilots
3c732f1 Vitest +14 cases on avatarInfo reducers
8b4308a tests: co-locate every Vitest suite next to its subject under src/
803de20 tests: flatten renderer mock to src/nitro-renderer.mock.ts (drop mocks/)

Phase 8: CI

SHA Subject
8844cc1 ci: run typecheck + Vitest on every push to main/feat/** and on every PR
53fc5f0 ci: create renderer symlink after yarn install, not before
5d7a20a ci: use absolute symlink target + check out feat/react19-event-bus on the renderer fork
cb7502f ci: opt the JavaScript actions into Node.js 24

Phase 9: PR #126 cherry-pick + asset infrastructure

SHA Subject
35b8493 vite: fail fast with a setup hint when the renderer SDK is missing
53f41cd 🆙 Fix wear badge in popup
52b0c90 Merge pull request #126 from duckietm/Dev
45620ca vite: actually split the renderer into its own chunk
cd8951e dev: serve game assets via sirv plugin and pre-init configuration
2053c8e 🆕 Added Reset password / Email and change username in user settings
3a7c9ba 🆙 Fix wear badge in popup
9ef6983 post cherry-pick: restore useEffectEvent wrapper + fix configuration import
622d73c docs: reflect PR #126 cherry-pick + boot/asset infrastructure
8e0bcce Add yarn preview script for serving the production build
7cf01b0 docs: refresh ARCHITECTURE + CLAUDE with this session's work
cc225bd docs: comprehensive refresh after the React 19 modernization round

Phase 10: WiredCreatorTools Zustand hoists + Toolbar fix

SHA Subject
c16ac1d wired-tools: hoist UI-only state flags to Zustand store
eb8d879 docs(claude): record wiredCreatorToolsUiStore adoption + new test count
82bccd4 wired-tools: hoist monitorSnapshot + polling reset to the Zustand store
7758af7 docs(claude): bump vitest count to 181/181 after monitorSnapshot cases
8182e06 wired-tools: hoist inspection selection (+ live state + action version) to the store
50fd908 docs(claude): bump vitest count to 187/187 after selection-hoist cases
0fc32a1 wired-tools: hoist variable-highlight toggle + overlays to the store
c1aafff docs(claude): bump vitest count to 190/190 after highlight-hoist cases
181ca09 wired-tools: hoist inline editor state (variables + managed holder) to the store
438b47d docs(claude): bump vitest count to 193/193 after editor-hoist cases
4ab38d3 toolbar: always-mount nav rows + drive show/hide via framer variants

Phase 11: Upstream sync + final picker hoists (2026-05-18 session)

SHA Subject
e209146 🆙 Update About screen (needs a emu change as well) (upstream)
b2318b9 🆕 Added support for JSON5 (upstream)
779a98c merge: sync upstream duckietm/Dev (b2318b9) into feat/react19-modernization
3b35fa9 docs(CLAUDE.md): refresh upstream-sync status after merging origin/Dev b2318b9
ba77806 wired-tools(store): hoist variable-key records (selectedInspectionVariableKeys, selectedVariableKeys)
8894fcc wired-tools(store): hoist inspection give pickers (inspectionGiveVariableItemId, inspectionGiveValue)
1c2d8da wired-tools(store): hoist managed-holder give picker chain

Phase 12: Snapshot consumer-side wiring + first migrations

SHA Subject
e7e8bcc docs: full changelog for feat/react19-modernization + feat/react19-event-bus
b2a86da feat(hooks/session): React-side consumer hooks for the renderer snapshot pattern
71a0eee refactor(hooks/session): migrate useSessionInfo to useUserDataSnapshot
36addbe fix(avatar-info): reactive Ignore/Unignore menu entry via useIsUserIgnored
02a396d docs(CLAUDE.md): refresh stale sections — snapshot consumer hooks + closed bugs

Nitro_Render_V3 — feat/react19-event-bus (22 commits, baseline 98b03aa)

SHA Subject
87cf478 feat(events,session): add React-friendly subscribe APIs and snapshot getters
c7a5aea chore(ts): bump TypeScript 5.8 → 6.0 and add tsgo for fast type-checking
ddb7222 chore: bump TypeScript pins to ^6.0.3 across all 12 workspaces + thumbmarkjs 1.9 + add CLAUDE.md
e82d3e0 chore(types): augment ImportMeta with glob signature
afb5f33 fix(api): IRoomSession.password + sendBackgroundMessage + optional chatColour
c37171a TS 5.7+ ArrayBuffer drift: cast where ArrayBufferLike leaked
08d1efa Drop dead sendWhisperGroupMessage — composer never existed
0fc38a1 Fix self-referential ConstructorParameters in two Wired composers
999b818 Fix PetBreedingMessageParser bytesAvailable check
b42f989 RoomEnterComposer: optional spawnX/spawnY for reconnect
5ea3201 Align with Pixi v8: Filter[] union, WebGLRenderer narrow, ImageLike
22d4e5b SocketConnection parser cast + RoomChatHandler arg-order fix
f7a5897 Renderer: align NitroConfig Window decl with client + fix glob .default access
ef6c661 Renderer: surface allowUnderpass on RoomSettingsData + composer
5f5ba2f docs(claude): document recent feat/react19-event-bus additions
b6a26fb 🆙 Small fix landscape's where a bit offset (upstream)
e3078f0 Merge pull request #69 from duckietm/Dev (upstream)
820f791 Merge remote-tracking branch 'origin/main' into feat/react19-event-bus
98662e7 test(utils): add BinaryReader / BinaryWriter round-trip coverage (23 cases)
a599e0c feat(session): snapshot getters for IgnoredUsersManager + GroupInformationManager
761d8ff feat(session): snapshot getter for UserDataManager room user list
892d16b feat(sound): snapshot getter + volume-update event on SoundManager
d740f83 refactor(parsers): flatten nested bytesAvailable guards on UserProfile + GetGuestRoomResult
28c552f docs(CLAUDE.md): document new snapshot getters + flat bytesAvailable pattern

7. Final state matrix

Repository state

Repo Branch HEAD Tracking Push status
Nitro-V3 feat/react19-modernization 02a396d simoleo/feat/react19-modernization up-to-date
Nitro_Render_V3 feat/react19-event-bus 28c552f fork/feat/react19-event-bus up-to-date
Arcturus-Morningstar-Extended main efb4997 (v4.1.16) origin/main up-to-date (no fork divergence)
NitroV3-Housekeeping (not touched)

Verification gates

Gate Client Renderer
Typecheck (yarn typecheck / yarn compile:fast — tsgo) clean (0 errors) clean (0 errors)
Vitest (yarn test --run) 203/203 127/127
Production build (yarn build) green n/a (library)

Test totals — evolution

The client started with 0 Vitest cases when the branch was opened. The renderer started with 0 too.

Milestone Client Renderer
Branch open 0 0
After Vitest setup (6793de2) 22
After +16 ColorUtils etc (bb28d25) 38
After FriendlyTime (dbafc97) ~50
After dedupeBadges (b1729d8) 56
After avatarInfo reducers (3c732f1) 70
After catalog helpers (fd3ef78) 104
After useCatalogFavorites (8b79233) 120
After mock layer + first hook test (c401839) ~133
After all hoists pre-upstream-sync (438b47d) 193
After Phase 11 picker hoists (1c2d8da) 203
Renderer Vitest baseline (utility suites) 104
After BinaryReader tests (98662e7) 127

Public-API additions on the renderer (consumed by the React client)

Surface Commit Type
EventDispatcher.subscribe(type, cb): () => void 87cf478 new
CommunicationManager.subscribeMessage(eventCtor, handler): () => void 87cf478 new
SessionDataManager.getUserDataSnapshot() + IUserDataSnapshot + SESSION_DATA_UPDATED 87cf478 new
RoomSessionManager.getActiveRoomSessionSnapshot() + IRoomSessionSnapshot + ROOM_SESSION_UPDATED 87cf478 new
IRoomSession.password (interface caught up to impl) afb5f33 interface fix
IRoomSession.sendBackgroundMessage(image, stand, overlay, card?) (interface caught up to impl) afb5f33 interface fix
IRoomSession.sendChatMessage / sendShoutMessagechatColour optional afb5f33 signature relax
RoomEnterComposer(roomId, password?, spawnX?, spawnY?) — 4-arg variant b42f989 extension
RoomSettingsData.allowUnderpass + parser + composer arg ef6c661 extension
IgnoredUsersManager.getIgnoredUsersSnapshot() + IGNORED_USERS_UPDATED a599e0c new
GroupInformationManager.getGroupBadgesSnapshot() + GROUP_BADGES_UPDATED a599e0c new
UserDataManager.getRoomUserListSnapshot() + ROOM_USER_LIST_UPDATED 761d8ff new
SoundManager.getVolumesSnapshot() + systemVolume/furniVolume getters + ISoundVolumesSnapshot + SOUND_VOLUMES_UPDATED 892d16b new

Bugs fixed during the modernization

Bug Commit Repo Severity
MainView CREATED/ENDED race (no session token guard) 9d10e52 client medium — could clear current session state
LayoutImage async fetch race when props change twice quickly 97c9717 client medium — stale image overwrites valid one
InfiniteGrid rules-of-hooks violation 5697d16 client low — lint, no functional bug
Avatar editor .paletteID null-deref b01f09c client low
Toolbar spam-toggle leaving children at opacity 0 / scale 0.8 4ab38d3 client medium — visible UI bug, also opened upstream as PR #130
Two logic bugs found during refactor (documented) 81656e7 client lowmedium
PetBreedingMessageParser.bytesAvailable < 12 comparing boolean against number 999b818 renderer high — incorrect parse on common path
ChatWhisperGroupComposer never existed but was declared on the interface 08d1efa renderer low — dead code
SoundManager volume diff comparison (percent vs fraction) 892d16b renderer medium — every settings push fired updateFurniSamplesVolume + musicController.updateVolume regardless of whether the volume changed
Two Wired composers had self-referential ConstructorParameters 0fc38a1 renderer low — typecheck only

Rollback safety tags (local-only, session-private)

Repo Tag Points at
Nitro-V3 pre-upstream-merge-20260518 4ab38d3
Nitro_Render_V3 pre-upstream-merge-20260518 5f5ba2f
Arcturus-Morningstar-Extended pre-upstream-pull-20260518 e6093f9