Many composer/parser pairs on the Nitro wire are correlation-key based:
the request carries a key (roomId, issueId, etc.) and the response shows
up on the globally-shared event bus, where other components may be
listening for the same parser type with a different key. The previous
useNitroQuery resolved on the FIRST matching parser event regardless of
key — useless for that pattern, which is why two obvious migration
targets (ModToolsChatlogView, CfhChatlogView) were skipped earlier.
Adapter change
- New optional `accept?: (event) => boolean` on NitroQueryConfig.
- In awaitNitroResponse, events for which accept returns false are
IGNORED rather than resolving the promise. The listener stays
registered, the timeout still applies. This lets callers do:
accept: e => e.getParser()?.data.roomId === roomId
Migrations
- src/components/mod-tools/views/room/ModToolsChatlogView.tsx
- Was: useState<ChatRecordData>(null) + useMessageEvent with
`if (parser.data.roomId !== roomId) return; setRoomChatlog(...)` +
a mount-only useEffect dispatching the composer.
- Now: a single useNitroQuery call keyed on roomId; accept filters
by roomId; the query is enabled only when roomId is set.
The composer is no longer re-dispatched on remount within
staleTime; switching to a different room still triggers a fresh
fetch because the queryKey changes.
- src/components/mod-tools/views/tickets/CfhChatlogView.tsx
- Same pattern, keyed on issueId.
Both migrations drop ~15 lines per file (no more local state + manual
listener + manual send) while gaining cache/dedup/loading/error
handling from TanStack Query.
Verification
- yarn eslint on the four files: 1 pre-existing error (the
IMessageEvent "redundant union" false positive in createNitroQuery
that we already documented — local sandbox doesn't have the
renderer SDK installed, so its types resolve as `any`).
- yarn test: 49/49 passing.
- yarn tsc on the four files: clean.
Phase 1 of the refactor plan in docs/ARCHITECTURE.md.
Install
- yarn add @tanstack/react-query@5 @tanstack/react-query-devtools@5
- Both pinned to ^5 (matches React 19 peer requirement).
Wiring
- src/index.tsx: mounts QueryClientProvider above ErrorBoundary +
Suspense. Default config: staleTime=30s, retry=1,
refetchOnWindowFocus=false (chat client, not a data dashboard).
Adapter
- src/api/nitro-query/createNitroQuery.ts: replaces the previous
prototype that just threw. Exposes:
* useNitroQuery({ key, request, parser, select, timeoutMs })
— wraps TanStack's useQuery; queryFn awaits the parser response.
* awaitNitroResponse(...) — lower-level helper for imperative use
via queryClient.fetchQuery.
The Promise:
1. registers the parser via GetCommunication().registerMessageEvent
2. dispatches the composer via SendMessageComposer
3. resolves with select(event) on the first matching parser
4. rejects after timeoutMs (default 15s)
5. always cleans up the listener + timeout (cancel-safe).
Pilot
- src/components/catalog/views/targeted-offer/OfferView.tsx:
the previous useMessageEventState + manual useEffect-send pattern
becomes a single useNitroQuery call. staleTime:Infinity because the
targeted offer doesn't change during a session. Subsequent OfferView
remounts (e.g. opening/closing the dialog) now reuse the cached
payload — the GetTargetedOfferComposer is no longer re-sent each
time.
Verification
- yarn eslint on the four touched files: 1 pre-existing
no-redundant-type-constituents error (IMessageEvent resolves as `any`
in the local sandbox without the renderer SDK installed; matches the
12 other pre-existing instances of the same false positive).
- yarn tsc on the four touched files: clean (modulo the
project-wide TS2307 about @nitrots/nitro-renderer).
- The original prototype's "throw" guard is gone — useNitroQuery is now
callable.
Migration path (per docs/ARCHITECTURE.md)
- Next adoption targets (read-only fetches first): useCatalog's page
data, useInventoryFurni's bot listing, Navigator search results,
Marketplace listings.
- Push messages (server-pushed events the client doesn't request)
keep using useNitroEventState / useMessageEventState — they're
subscriptions, not requests.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
This is the structural plan promised in the previous session, with concrete
pilots for all five proposals + the bonus error-boundary work.
== docs/ARCHITECTURE.md (new, ~370 lines)
Living document describing:
- where the project stands today (event-bus pattern friction with React 19,
god-hooks, oversized files);
- the five proposed structural improvements with the why/how/status of each;
- what's already in place across this branch;
- recommended order for the next refactor PRs.
This is the deliverable the rest of this commit references.
== Proposal #3 + #4 pilots: src/features/doorbell/ (new)
Concrete feature-folder migration on the doorbell widget (chosen because
it's small enough to migrate end-to-end in one commit).
src/features/doorbell/
index.ts public API
views/DoorbellWidgetView.tsx
hooks/useDoorbellState.ts reduces 3 events into a users array (data only)
hooks/useDoorbellActions.ts answer(name, flag) (imperative actions only)
The split (data vs actions) is the pattern proposal #4 wants applied to
useCatalog/useChat/useWiredTools later. The original useDoorbellWidget had
both concerns + a buggy `useEffect(() => setIsVisible(!!users.length), [users])`
derive-state-in-effect. The new view computes visibility in render.
Compat shims kept so existing imports keep working:
- src/components/room/widgets/doorbell/DoorbellWidgetView.tsx -> 1-line re-export
- src/hooks/rooms/widgets/useDoorbellWidget.ts -> deprecated wrapper around
the two new hooks, returning the same { users, answer } shape.
== Proposal #2 prototype: src/api/nitro-query/ (new)
Adapter outline for wrapping composer/parser request-response pairs in
TanStack Query. Not yet enabled because @tanstack/react-query is not in
package.json. The file documents the activation steps:
yarn add @tanstack/react-query @tanstack/react-query-devtools
+ mount QueryClientProvider in src/index.tsx
awaitNitroResponse() throws with a helpful pointer to the doc section if
called before activation, so accidental adoption fails loudly.
== Proposal #5 skeleton: src/state/createNitroStore.ts (new)
Same pattern: skeleton + activation instructions. Not yet enabled because
zustand is not in package.json.
yarn add zustand
+ replace the throw with `import { create } from 'zustand'; export const createNitroStore = create;`
The doc inside the file shows the recommended slice shape and points to
the suggested first migration target (the let isCreatingRoom singleton in
NavigatorRoomCreatorView).
== Bonus: WidgetErrorBoundary
src/common/error-boundary/WidgetErrorBoundary.tsx wraps react-error-boundary
with a sensible default (silent fallback, NitroLogger.error). Re-exported
from src/common/index.ts.
Applied as the umbrella around RoomWidgetsView's children — a widget
crash in a room (e.g. malformed pet data) now degrades gracefully
instead of unmounting the whole UI.
== Verification
- yarn eslint on all new + modified files: 0 errors / 0 warnings introduced.
RoomWidgetsView still has its 1 pre-existing FC<{}> error (1 before, 1 after).
- yarn tsc on all new files: clean (only project-wide pre-existing
TS2307 about @nitrots/nitro-renderer not installed locally remains).
- No regressions: existing imports of DoorbellWidgetView and
useDoorbellWidget keep resolving via the compat shims.
== What's NOT in this commit (intentionally)
- Mass adoption of the new patterns elsewhere — left as follow-up PRs in
the order documented in ARCHITECTURE.md "How to pick the next refactor PR".
- Installation of @tanstack/react-query / zustand — explicit team decision,
not the LLM's to make.
- Test infrastructure (Vitest setup) — listed as the #1 missing piece in
the doc, but a separate PR.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
Eliminate the four remaining missing-dependency warnings reported by
react-hooks v7. Each one was a real stale-closure or re-trigger hazard;
the fix matches the intent rather than just silencing the linter.
- src/App.tsx (line 448): wrap showSessionExpired with useEffectEvent
(onSessionExpired) so the prepare effect doesn't re-run on every
showSessionExpired identity change but still calls the latest
callback. Replace the two in-effect call sites.
- src/components/furni-editor/views/FurniEditorSearchView.tsx: wrap
the on-mount onSearch('', '', 1) call with useEffectEvent so the
callback prop isn't a missing dependency.
- src/components/notification-center/views/bubble-layouts/
NotificationBadgeReceivedBubbleView.tsx: wrap the
"fetch badges only if empty on mount" check with useEffectEvent
so badgeCodes.length isn't required as a dep (and won't re-fetch
every count change).
- src/components/navigator/views/room-settings/
NavigatorRoomSettingsRightsTabView.tsx: switch deps from
roomData?.roomId to roomData (the body uses roomData.roomId after
an early return; the linter wanted the whole object).
- src/api/ui-settings/UiSettingsContext.tsx: hoist ALL_CSS_VARS
outside the component (it's a static constant).
After this, yarn eslint reports zero exhaustive-deps warnings across
the whole src/.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
Run eslint --fix across src/ to clear ~1900 mechanical lint errors
surfaced by the @typescript-eslint v8 + react-hooks v7 + react-compiler
upgrade in the React 19 modernization PR.
Issues fixed automatically:
- brace-style (Allman): try/catch one-liners reformatted to multi-line
- indent: tab-vs-space and depth corrections
- semi: missing trailing semicolons
- no-trailing-spaces
No semantic changes. Remaining 701 errors are real-code issues
(set-state-in-effect, rules-of-hooks, no-unsafe-* type checks) that
need manual per-file review.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
Adopt React 19 idioms across the codebase. The runtime was already on
react@19.2.5 but no React 19 APIs were in use.
- forwardRef -> ref-as-prop in 7 layout/component files
(NitroInput/Button/ItemCountBadge/Card×5/InfiniteGridItem,
ToolbarItemView, AvatarEditorIcon)
- <Ctx.Provider> -> <Ctx> in 6 contexts (CatalogAdmin, FloorplanEditor,
UiSettings, GridContext, NitroCardContext, NitroCardAccordionContext)
- Native <script> hoisting for Turnstile, ExternalPluginLoader, GoogleAdsView
(React 19 dedupes by src; 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
- BackgroundsView migrated to use(promise) as a demonstrator pattern
for Suspense-driven config loading
- ESLint react setting 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)
- Refresh public/configuration/{asset-loader,bootstrap}.js to match
current write-asset-loader.mjs output
Phase 3 (login forms -> useActionState/useFormStatus) deferred:
LoginView is 1623 lines with lockout + Turnstile + heartbeat
interleaving; safer as its own PR.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
Adds a "Cards" tab to the Profile Background picker (BackgroundsView)
that selects a pattern applied to the entire user info card and the
extended profile container, in addition to the existing avatar-pad
background/stand/overlay layers.
- AvatarInfoUser/Utilities: propagate cardBackgroundId from RoomUserData.
- InfoStandWidgetUserView: stateful cardBackgroundId, applied as
.profile-card-background.card-background-{id} on the outer Column
with bg-color suppressed when active.
- UserContainerView: same class on the wrapper of the extended profile.
- BackgroundsView: 4th tab "cards" backed by cards.data config
(falls back to backgrounds.data); sends 4-id message via the
extended sendBackgroundMessage signature.
- ui-config.example: cards.data dataset (15 entries).
- BackgroundsView.css: 188 .card-background-{N} rules cloned from
background-{N} (repeat-tiled) plus 15 CSS-pattern overrides for the
provisional dataset (gradients, stripes, dots, grid, checker).
- add the new :wired inspection/monitor panel with furni, user and global tabs
- add live variables, previews, inline editing and keep-selected behavior
- add global room diagnostics placeholders, monitor artwork and server/client timezone display
- add editor support for wf_xtra_text_output_furni_name and related UI texts/assets