End-to-end documentation of every modification since the two branches were opened: - 109 commits on feat/react19-modernization (baseline ae17619) - 22 commits on feat/react19-event-bus (baseline 98b03aa) - the 2026-05-18 Arcturus FF pull to v4.1.16 Organized into 11 phases on the client (React 19 baseline → infra pillars → god-hook splits → WiredCreatorTools extraction + Zustand hoists → typecheck cleanup → error boundaries → test infrastructure → CI → PR #126 cherry-picks + asset middleware → toolbar spam-toggle fix (PR #130 upstream) → full upstream sync + final picker hoists) and 9 phases on the renderer (v2.1.0 React-friendly API → TS 6/tsgo → API interface alignments → ArrayBuffer drift → Pixi v8 → composer/parser alignment with Arcturus → dead code → upstream sync → snapshot extensions). Includes the full commit index per branch, the public-API additions table, the bugs-fixed table with severity, the Vitest test-count evolution (0 -> 203 client, 0 -> 127 renderer), and the local rollback-tag list.
64 KiB
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, 109 commits since baselineae17619 - Nitro_Render_V3 (renderer library) — branch
feat/react19-event-bus, 22 commits since baseline98b03aa
Plus the in-session upstream sync of the third codebase touched on 2026-05-18:
- Arcturus-Morningstar-Extended (Java emulator) — FF pull
e6093f9→efb4997(v4.1.16)
Working directory: E:\Users\simol\Desktop\DEV. (NitroV3-Housekeeping was not touched during the lifetime of these branches.)
Table of contents
- Overview
- Nitro-V3 client — branch story
- Phase 1: React 19 baseline adoption
- Phase 2: Infrastructure pillars (Query, Zustand, Vitest, mocks, Form Actions)
- Phase 3: Hook taxonomy and god-hook splits
- Phase 4: WiredCreatorTools extraction and Zustand hoists
- Phase 5: Typecheck cleanup (Pixi v8, TS 6, framer-motion)
- Phase 6: Error boundaries and logic-bug fixes
- Phase 7: Test infrastructure evolution
- Phase 8: CI pipeline
- Phase 9: Upstream cherry-picks (PR #126) and drive-by bugs
- Phase 10: Toolbar spam-toggle fix (PR #130 upstream)
- Phase 11: Full upstream sync (origin/Dev b2318b9)
- Nitro_Render_V3 renderer — branch story
- Phase 1: v2.1.0 React-friendly API additions
- Phase 2: TypeScript 6 + tsgo migration
- Phase 3: API interface alignments (IRoomSession)
- Phase 4: TS 5.7+ ArrayBuffer drift fixes
- Phase 5: Pixi v8 alignment
- Phase 6: Composer/parser alignment with Arcturus
- Phase 7: Dead code removal and small fixes
- Phase 8: Upstream sync (origin/main)
- Phase 9: Snapshot pattern extensions
- Arcturus emulator — upstream pull
- Documentation evolution (CLAUDE.md / ARCHITECTURE.md)
- Full commit index
- 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 |
1c2d8da |
109 (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
- Stay on the classic
src/components/+src/hooks/layout — an early experiment withsrc/features/<feature>/was reverted (commit0755285); the team decided the in-place layout is the convention. Every PR that violates it gets reworked. - God-hook split into 3 files, flat in the hooks barrel directory —
use<Feature>State,use<Feature>Actions,use<Feature>Widget(deprecated wrapper). No per-feature subfolders for hooks. useBetween-based singleton-filter pattern — for hooks shared by many consumers but where most consumers only need state OR actions, not both: one internaluseBetweenStore, then auseStatefilter,useActionsfilter, and a deprecateduseFooshim.- Renderer-SDK mock for Vitest —
src/nitro-renderer.mock.tsaliased over@nitrots/nitro-rendererviavitest.config.mts. Without it, importing anysrc/api/*file in a test crashes jsdom because the real renderer eagerly loads Pixi v8 at module-import time. - Tests co-located next to subjects —
src/path/Foo.ts↔src/path/Foo.test.ts. No paralleltests/tree. - Snapshot pattern on the renderer — referentially-stable, lazy-frozen, invalidated-on-mutation. Now extended to 6 state holders (Session, RoomSession, IgnoredUsers, GroupInfo, RoomUserList, SoundVolumes).
- 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 bysrcand removes manualdocument.head.appendChild+ module-level promise caches. - React Compiler enabled at build time via
babel-plugin-react-compilerinvite.config.mjs(target'19'), pluseslint-plugin-react-compilerin lint mode. - Global
<ErrorBoundary>+<Suspense>insrc/index.tsxusingreact-error-boundary, withLoadingViewas fallback. use(promise)pilot inBackgroundsViewdemonstrating Suspense-driven config loading.- ESLint react settings bumped 18.3.1 → 19.2; legacy
@typescript-eslint/ban-typesreplaced withno-restricted-types(the old rule was removed in@typescript-eslintv8).
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 form —
handleLoginSubmit → loginAction(prevState, FormData)wrapped inuseActionState. Submit button extracted as<LoginSubmitButton/>reading pending state viauseFormStatus. Reads username/password/remember from FormData; remember checkbox carriesname="remember". - Forgot password form —
forgotActionwrapped inuseActionState; awaits parentonSubmitso pending stays true through the parent fetch.ForgotSubmitButtonusesuseFormStatus. - Register credentials step —
credentialsActionwithuseActionState; the step transition (setStep('avatar')) happens inside the action afterpingServer+onCheckEmail. - Register avatar step —
avatarActionvalidates username, pings server, checks availability, then awaitsonSubmit. Button label usesisAvatarPendingto show "Creating…" without prop drillingsubmitting. DialogSharedProps.onSubmitsignatures updated to returnPromise<void>so dialog actions canawaitthe parent's fetch.lockStatememo replaced with a directreadLock()call in render — any re-render (triggered by the action's pending toggle) recomputes it.- Unused
FormEventimport dropped; unusedcheckingstate in RegisterDialog dropped.
Other Phase 1 commits
535fa71— ESLint--fixauto-fix for brace-style/indent/semi/no-trailing-spaces (mechanical hygiene before adopting new lint rules).25d51af— Enabled<StrictMode>+ madeApp.tsxrenderer 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 inInfiniteGrid.6c9f414— ApplieduseEffectEvent(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 fastyarn typecheckscript alongside TS 6.d382635— Phase A: cleared allreact-hooks/exhaustive-depswarnings viauseEffectEventor 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 folder —
src/features/doorbell/withviews/,hooks/useDoorbellState.ts,hooks/useDoorbellActions.ts. The split (data vs actions) became the canonical pattern. The folder structure itself was later reverted (commit0755285); 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.tsxmountsQueryClientProvideraboveErrorBoundary + Suspense. Default config:staleTime=30s,retry=1,refetchOnWindowFocus=false(chat client, not a data dashboard).- New adapter at
src/api/nitro-query/createNitroQuery.ts. ExposesuseNitroQuery({ key, request, parser, select, timeoutMs })(wraps TanStack'suseQuery) andawaitNitroResponse(...)(lower-level helper). The internal promise registers the parser, dispatches the composer, resolves withselect(event)on first matching parser, rejects aftertimeoutMs(default 15s), and always cleans up. - First pilot —
OfferViewmigrated from the previoususeMessageEventState + manual useEffect-sendpattern.
fd1835c — Enable Zustand (proposal #5) + convert isCreatingRoom singleton
yarn add zustand(^5).src/state/createNitroStore.tsexports a re-export of zustand'screateunder the project-local namecreateNitroStore. Comments document the convention (one store per domain, subscribe to slices not the whole store).- First migration target —
src/components/navigator/views/navigatorRoomCreatorStore.ts. A Zustand store withisCreating: booleanandbeginCreate()(latches the flag, dispatches an auto-resetsetTimeoutafter 5s, replaces any in-flight timer on re-entry). The component drops two module-levelletvariables 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 fromvite.config.mjsbecause the test runner shouldn't pull in the renderer SDK aliases).tests/setup.tsimports@testing-library/jest-dom/vitestfor 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:
useExternalSnapshotfor subscribing to renderer-side snapshot getters viauseSyncExternalStore.useNitroEventReducer/useMessageEventReducerfor stateful event accumulation (reducer-style).
Other Phase 2 commits
9d2e4a7— Expanded Vitest coverage on the pure helpers insrc/api/{utils,wired}.388fb8e— MigratedCatalogLayoutRoomAdsView's room-ad fetch touseNitroQuery.bf84a0c—useNitroQueryextended with anaccept()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— AddedCLAUDE.md+ refresheddocs/ARCHITECTURE.md.559d860— Pilot: moved InfoStand event listeners touseAvatarInfoWidgetowner.8b7bedf— Pilot: extracteduseInventoryFurnireducers to a pure module.b1729d8— Vitest: covereddedupeBadgeswith 6 cases.f1af6fb— Docs: ARCHITECTURE pattern #1 — companions implemented, pilots adopted.8e4544c— Migratedcatalog/giftConfigurationtouseNitroQuery.
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):
0ae371e—useFurniChooserWidgetsplit.85fc827—useUserChooserWidgetsplit.f3442f8—useFriendRequestWidgetsplit.7218285—usePollWidgetsplit into subscriptions + actions (proposal #4).419de09— HoistedusePollSubscriptionstoRoomWidgetsView; dropped the side effect fromusePollWidget.a4c9dd8—useChatInputWidgetsplit.
useBetween-based singleton-filter splits for hooks shared by many consumers:
e1f5df6—useWiredToolssplit into state + actions viauseBetweensingleton.eeb9cc6—useTranslationsplit viauseBetweensingleton.5344eaf—useNotificationsplit viauseBetweensingleton.9f3cd9b—useFriendssplit viauseBetweensingleton.
Catalog three-way split (the most extensive decomposition)
fd3ef78— Extracted pure helpers fromuseCatalog(buildCatalogNodeTree,findNodeById/findNodeByName,getNodesByOfferIdFromMap,getOfferProductKeys,normalizeCatalogType,resolveBuilderFurniPlaceableStatus) intosrc/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 36useCatalog()consumers to the three filters. DeprecateduseCatalogshim 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
2d9785e—useUserGroups: consolidated 4 dedup'dCatalogGroupsComposercall sites.2a5b9a4—useClubOffers: per-windowIdTanStack query for HC offer pages.3947781—useSellablePetPalette(breed): per-breed TanStack query for pet picker.9a807bf—useMarketplaceConfiguration: lifted the marketplace config self-fetch.7b06229—useClubGifts+useNitroEventInvalidator: closed the catalogOptions bag (composer/parser request-response with server-driven invalidation).8b79233— ExtracteduseCatalogFavoritespure 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— SplitWiredCreatorToolsView: extracted types/constants/helpers into 3 sibling files (WiredCreatorTools.types.ts,WiredCreatorTools.constants.ts,WiredCreatorTools.helpers.ts).23fc302— Extracted Variables tab JSX intoWiredVariablesTabViewcomponent.d7d9a7e— Extracted Inspection tab JSX intoWiredInspectionTabViewcomponent.bb09a56— Extracted Monitor tab JSX intoWiredMonitorTabView+ dropped dead overlays.
Progressive Zustand hoists (each its own commit for revertability)
c16ac1d— UI flags hoisted touseWiredCreatorToolsUiStore: 14 pure UI flags (isVisible,activeTab,inspectionType,variablesType, modal/popover opens, monitor and variable-manage filters/sort/page). Setters accept value-or-updater.WiredInspectionTabViewandWiredVariablesTabViewdrop 6 props.eb8d879— Docs follow-up (CLAUDE: store adoption + test count bump).82bccd4— monitorSnapshot hoisted + polling reset. Server-pushed stats now survive panel close/reopen.7758af7— Docs: vitest count bump after monitorSnapshot cases.8182e06— Selection hoisted (selectedFurni,selectedFurniLiveState,selectedUser,selectedUserLiveState,selectedUserActionVersion). Listeners (useObjectSelectedEvent, per-kinduseMessageEventhandlers) stay in the component (need React lifecycle) and call store actions. Live-state setters keep theUpdater<T>shape so the ~10previousValue => ...call sites stay verbatim.50fd908— Docs: vitest count bump.0fc32a1— Variable-highlight hoisted (isVariableHighlightActivetoggle +variableHighlightOverlaysscreen-coords array).WiredVariablesTabViewdrops two more props. The two screen-coords recompute effects stay in the component (need React lifecycle forWiredSelectionVisualizerinstall/teardown).variableHighlightObjectsRefstays asuseRef(refs don't belong in Zustand).c1aafff— Docs: vitest count bump.181ca09— Inline editor hoisted (editingVariable/editingValue/editingManagedHolderVariableId/editingManagedHolderValue).WiredInspectionTabViewdrops three more props.shouldPauseVariableSnapshotRefreshstill 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)
ba77806— Variable-key records hoisted (selectedInspectionVariableKeys,selectedVariableKeys). Setter shapeUpdater<Record<...Type, string>>because all writers usedprev => ({ ...prev, [type]: key }). Empty defaults — the existing definition-sync effect atWiredCreatorToolsView.tsx:1543-1574populates them on first render. +4 test cases.8894fcc— Inspection give pickers hoisted (inspectionGiveVariableItemId,inspectionGiveValue). Plain typed setters, sentinel pair0/'0'. +2 test cases.1c2d8da— Managed-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— Typedframer-motion variantsasVariants— killed 33 tsgo errors.96b61ff— Fixed 4 typecheck errors increateNitroQuery.feba672— Sweep: union expansions + React 19 JSX + extra arg.1083b2e— TypeduseFurniChooserStatebuilders + dropped deadgetUserDataguard.a39aa37— React 19:useRef<T>() → useRef<T>(null)across 15 sites (React 19 made this required at the type level).f57266a— Updated 3IGetImageListener.imageReadycall sites to Pixi v8 single-arg signature.a8065f6— Added optionalclone()toIPurchasableOffer.71a1586— Stripped dead server-sync fromUiSettingsContext+ re-exportedui-settings.0192952— Sweep: 11 fixes across 9 files.2a9a5dd— Addedreact-colorfuldep forInterfaceColorTabView.f09bb7e— Pixi v8 alignment in 2 room-widget helpers.0c43377— Dropped deadawait successon 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 ownWidgetErrorBoundaryso 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 indocs/ARCHITECTURE.md.9d10e52— Fix(MainView): collapsed CREATED/ENDED listeners into a session-aware reducer. The previous two-effect pattern had a race: aRoomSessionEvent.ENDEDfor a stale session could clear the current session's state if it arrived afterCREATED. The reducer now compares session tokens.97c9717— Fix(layout-image): guarded async image fetch with arequestIdRef. 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.b01f09c—fix: null-check the set type before reading.paletteIDin 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.b1729d8—dedupeBadgeswith 6 cases.3c732f1—avatarInforeducers with 14 cases.c401839— Renderer-SDK mock layer attests/mocks/renderer-mock.ts(later flattened tosrc/nitro-renderer.mock.ts). Stubs:- Explicit behavioral stubs for symbols tests actually exercise (
NitroLogger,GetEventDispatcher,mockEventDispatcherhelpers,RoomSessionDoorbellEvent). - String-keyed Proxy enums for
NitroEventType,RoomObjectCategory,AvatarFigurePartType. - Lightweight
class StubClass {}placeholders for ~30 Pixi and gameplay classes thesrc/api/*barrel touches. - Singleton getters returning chainable Proxies.
- First 2 component-/hook-level pilots:
WidgetErrorBoundary(4 tests) +useDoorbellState(7 tests).
- Explicit behavioral stubs for symbols tests actually exercise (
fd3ef78— Catalog pure-helper extraction + 34 Vitest cases.8b79233—useCatalogFavoritespure helpers + 16 Vitest cases.8b4308a— Tests co-located undersrc/— every*.test.ts(x)moved next to its subject.803de20— Flattened renderer mock tosrc/nitro-renderer.mock.ts(dropped__mocks__/).
Phase 8: CI pipeline
8844cc1—ci: ran typecheck + Vitest on every push tomain/feat/**and on every PR. Workflow at.github/workflows/ci.yml.53fc5f0—ci: created renderer symlink AFTERyarn install, not before (yarn install would otherwise nuke the symlink).5d7a20a—ci: used absolute symlink target + checked outfeat/react19-event-buson the renderer fork.cb7502f—ci: 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 reachedb2318b9).53b0c9053f41cd2053c8e— Fix wear badge popup +UserAccountSettingsView(reset password / email / username under user settings).3a7c9ba— Same wear-badge popup fix (rebased version).9ef6983— Post-cherry-pick: restoreduseEffectEventwrapper + 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:
45620ca—vite: actually split the renderer into its own chunk.cd8951e—dev: served game assets viasirvplugin and pre-init configuration. The chokidar-on-177k-files problem on Windows: serving game assets throughsirvmiddleware mounted on/nitro-assetsand/swfreading fromE:\Users\simol\Desktop\DEV\Nitro-Files\bypasses chokidar entirely. The plugin also wires the same handler intoconfigurePreviewServer. Same commit introducedawait GetConfiguration().init()insrc/bootstrap.tsbefore importing./index— otherwise the first paint flooded with "Missing configuration key" warnings while components synchronously read keys against an empty store.35b8493—vite: failed fast with a setup hint when the renderer SDK is missing.8e0bcce— Addedyarn previewscript 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)
4ab38d3 — toolbar: 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:
containerVariantsdropped its'exit'state (now lives in'hidden').itemVariantsdropped'exit'as well.- New
shellVariantsfor the backplate. pointer-eventsanimated 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.hiddendepends onisInRoom(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 needingAnimatePresence. - Inner
AnimatePresencearound 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-20260518at4ab38d3. git merge --no-ff origin/Devproduced 6 conflicts inpackage.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 (
json5dep,JSON5.parsefallback inbootstrap.ts,baseconfig invite.config.mjs). ThepersistAccessTokenFromPayload(payload)upstream fix inLoginView.tsxwas 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 (
cdf8d92upstream =2053c8elocal byte-for-byte). - Five files came in upstream-only with no local divergence.
yarn.lockregenerated viagit checkout --ours yarn.lockthenyarn 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).
3. Nitro_Render_V3 renderer — branch story
Phase 1: v2.1.0 React-friendly API additions
87cf478 — feat(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 theuseSyncExternalStoresubscribe signature. LegacyaddEventListener/removeEventListenerstill work.CommunicationManager.subscribeMessage(eventCtor, handler): () => void— packet-stream equivalent.SessionDataManager.getUserDataSnapshot(): Readonly<IUserDataSnapshot>— referentially-stable read-only view invalidated through the newSESSION_DATA_UPDATEDevent.RoomSessionManager.getActiveRoomSessionSnapshot(): Readonly<IRoomSessionSnapshot> | null— same pattern, invalidated throughROOM_SESSION_UPDATED.
The interface contracts:
packages/api/src/nitro/session/IUserDataSnapshot.tspackages/api/src/nitro/session/IRoomSessionSnapshot.ts
Bumped renderer to 2.1.0.
Phase 2: TypeScript 6 + tsgo migration
c7a5aea— Bumped TypeScript5.8 → 6.0. Added@typescript/native-preview(tsgo, the TS 7 preview compiler) asyarn 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
cryptocalls).
- Removed
ddb7222— Bumped TypeScript pins to^6.0.3across all 12 workspaces + thumbmarkjs 1.9. AddedCLAUDE.mdto the renderer.e82d3e0—chore(types): augmentedImportMetawithglobsignature.
Phase 3: API interface alignments (IRoomSession)
afb5f33 — fix(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 inRoomSessionManager).sendBackgroundMessage(backgroundImage, backgroundStand, backgroundOverlay, backgroundCard?)— sends the profile-background composer (used by the React client'sBackgroundsView).sendChatMessage/sendShoutMessagechatColourparameter relaxed to optional. The implementation already acceptedundefined; 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
c37171a — TS 5.7+ ArrayBuffer drift: cast where ArrayBufferLike leaked. The renderer never uses SharedArrayBuffer, so the type-level narrowings are safe. Sites cast:
BinaryReader/BinaryWriter—getBuffer()/toArrayBuffer().WsSessionCrypto.randomNonce().ArrayBufferToBase64.
Phase 5: Pixi v8 alignment
5ea3201 — Align with Pixi v8: Filter[] union, WebGLRenderer narrow, ImageLike. Four sites where Pixi v8's stricter typing tripped tsgo:
AvatarImage:container.filtersisreadonly Filter[] | nullin v8. Old fallback branchelse 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 viathis.getSprite(layerId)inside the override body.ExtendedSprite:renderer.gl/glRenderTarget.resolveTargetFramebufferexist only onWebGLRenderer/GlRenderTarget. The runtime checkrenderer.type === RendererType.WEBGLguarantees this; cast at the boundary.TextureUtils.generateImage: Pixi v8'sExtractor.image()returns the unionImageLike(HTMLCanvasElement | HTMLImageElement); the public signature promisesHTMLImageElement. Cast at return.
Phase 6: Composer/parser alignment with Arcturus
b42f989— RoomEnterComposer: optionalspawnX/spawnYfor reconnect. Arcturus'RequestRoomLoadEventreads 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 insideRoomSession/RoomSessionManager— the composer signature was lagging behind.0fc38a1— Fixed self-referentialConstructorParametersin two Wired composers (WiredRoomSettingsRequestComposer,WiredUserVariablesRequestComposer— empty-tuple composers needed explicitgetMessageArray(): []annotation).999b818— FixedPetBreedingMessageParser.bytesAvailable < 12(bytesAvailable is a boolean, not a byte count — the old code compared it against12which TS 6 caught).ef6c661— Renderer: surfaceallowUnderpasson RoomSettingsData + composer. Arcturus'RoomSettingsComposerappends an extra int at the end of the payload —room.isAllowUnderpass() ? 1 : 0— andRoomSettingsSaveEventoptionally 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: alignedNitroConfigWindow declaration with the client + fixed glob.defaultaccess.
Phase 7: Dead code removal and small fixes
08d1efa— Drop deadsendWhisperGroupMessage.IRoomSession.sendWhisperGroupMessage(userId)referenced aChatWhisperGroupComposerthat 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 isRoomUnitChatWhisperComposer(recipientName, message, styleId)— unchanged.5f5ba2f— Docs: documented recentfeat/react19-event-busadditions 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-20260518at5f5ba2f. git merge --no-ff origin/mainauto-completed with no conflict prompts.- Commit
820f791. - Verification:
yarn compile:fastclean, 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'sDataInputStream) - string round-trip with length prefix + bare (
includeLength=false) + UTF-8 multibyte byte count + empty-string edge writeBytesfor bothnumber[]andArrayBufferpayloadsreadBytesslice returns an independent reader whose position is decoupled from the outer readerremaining()decrements correctly across mixed-size readsreadFloat/readDoubledecode IEEE-754 big-endian values (the writer has no float/double counterparts — buffer built viaDataViewfor these cases)- writer
positiongetter + 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 alongsideaddUserToIgnoreListfor result code 2 (queue truncation) — added explicitinvalidateIgnoredUsersSnapshot()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 nestedifwith 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 nestedif. 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 files010_Wired_Update.sql,018_Last_Username_Change.sql, and a renamed019_custom_nick_login_tokens_wired_message.sql. - New auth-related Java classes:
AccountChangeEndpoints,AccountCheckEndpoints,AuthHttpUtil,CorsOriginGate,RegistrationSupport,SessionEndpoints,StaticContentEndpoints. ChangeNameCommand.javaremoved (replaced by API endpoints).- Updates to
AboutCommand,CommandHandler,GuildManager, multiple guild event handlers,WebSocketChannelInitializer. - Default DB file renamed
FullDB.sql→FullDatabase.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 |
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 |
1c2d8da |
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 / sendShoutMessage — chatColour 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 | low–medium |
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 |