mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
docs(claude,architecture): refresh snapshot adoption status after 2026-05-19 fix
The earlier "BLOCKED" / "rolled back" framing in CLAUDE.md +
ARCHITECTURE.md is stale: the three pilot snapshot-consumer migrations
shipped in d28819d on 2026-05-19 once the root cause was pinpointed
(`use-between` 1.x ships a dispatcher proxy that doesn't implement
`useSyncExternalStore`, so any snapshot hook called inside
useBetween(stateFn) crashes the first render).
Updated:
- CLAUDE.md → "Patterns to use → useSessionSnapshots": rewrote the
adoption-status paragraph to record the three live consumers, the
hard structural constraint (snapshot reads MUST be outside
useBetween scope, with the precise dispatcher line numbers + the
exact error fingerprint), and the fix template applied to
useSessionInfo (outer wrapper reads the snapshot, inner state
function keeps only use-between-safe hooks).
- CLAUDE.md → "What's wired up and what isn't" tables:
- Adopted row for "Renderer snapshot consumer hooks" lists the
three live consumers instead of the old "No in-tree consumers"
note.
- "Not yet" row renamed from "Blocked" to "Unblocked — migrate more
consumers", with concrete next candidates
(GetSessionDataManager().userId / userName / clubLevel /
securityLevel, GetRoomSessionManager().getActiveSession(),
GetSoundManager().<volume>) and a reminder of the constraint
+ the CI gate that enforces it.
- useChatWidget.ownUserId row notes the reactive migration via
useUserDataSnapshot landed (direct hook call — useChatWidget
isn't wrapped in useBetween, so the constraint doesn't apply).
- ARCHITECTURE.md → "useExternalSnapshot" subsection: replaced the
2026-05-18 rollback note with the structural constraint + the
2026-05-19 fix landing, including pointers to the regression test
and the new CI gate (eslint.hooks.config.mjs + yarn lint:hooks).
No code change in this commit — yarn typecheck clean, yarn
lint:hooks clean.
This commit is contained in:
@@ -150,16 +150,54 @@ a stale renderer bundle and degrades to a frozen default snapshot if
|
|||||||
the renderer doesn't expose the matching getter (kept module-level so
|
the renderer doesn't expose the matching getter (kept module-level so
|
||||||
React's bailout still works on the degraded path).
|
React's bailout still works on the degraded path).
|
||||||
|
|
||||||
**Adoption status: zero in-tree consumers.** The first three pilot
|
**Adoption status: three pilot consumers shipped (commit `d28819d`,
|
||||||
migrations (`useSessionInfo`, `useChatWidget.ownUserId`,
|
2026-05-19).** `useSessionInfo` reads userFigure / respectsLeft /
|
||||||
`AvatarInfoWidgetAvatarView` Ignore/Unignore) were rolled back in
|
respectsPetLeft from `useUserDataSnapshot`; `useChatWidget.ownUserId`
|
||||||
`e142efd` after a persistent runtime error
|
reads from the snapshot directly; `AvatarInfoWidgetAvatarView` flips
|
||||||
`(intermediate value)() is undefined` at `ToolbarView.tsx:46` that the
|
its Ignore/Unignore menu via `useIsUserIgnored`.
|
||||||
vite-alias fix (`790ad2b`) and the defensive guards (`c35a2d4`) could
|
|
||||||
not eliminate. The hooks remain available for any future opt-in
|
The original rollback (`e142efd`) was caused by a hard structural
|
||||||
consumer, but **do not migrate `useBetween`-shared consumers to them
|
constraint, NOT a stale renderer or React Compiler quirk: **snapshot
|
||||||
without isolated testing first** — that combination is the suspected
|
hooks (`useSyncExternalStore`-based) must NOT be called inside a
|
||||||
cause of the bug.
|
`useBetween(stateFn)` scope.** `use-between` 1.x swaps
|
||||||
|
`ReactCurrentDispatcher.current` with its own proxy
|
||||||
|
(`ownDispatcher` at
|
||||||
|
`node_modules/use-between/release/index.esm.js:54-169`) that
|
||||||
|
re-implements only useState / useReducer / useEffect /
|
||||||
|
useLayoutEffect / useCallback / useMemo / useRef /
|
||||||
|
useImperativeHandle. `useSyncExternalStore` isn't on the list, so
|
||||||
|
React resolves `dispatcher.useSyncExternalStore` to `undefined` and
|
||||||
|
crashes on first paint — that's the original "(intermediate value)()
|
||||||
|
is undefined" at `ToolbarView.tsx:46`. Chrome reports the same as
|
||||||
|
`dispatcher.useSyncExternalStore is not a function`.
|
||||||
|
|
||||||
|
**Fix pattern, applied to `useSessionInfo`:** call the snapshot hook
|
||||||
|
in the OUTER exported wrapper, after `useBetween`, so it runs in the
|
||||||
|
real React dispatcher's scope. The inner state function (the one
|
||||||
|
`useBetween` actually proxies) keeps only useState /
|
||||||
|
useMessageEvent / plain actions.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const useSessionInfoState = () => {
|
||||||
|
// ONLY use-between-safe hooks here.
|
||||||
|
const [chatStyleId, setChatStyleId] = useState(0);
|
||||||
|
// … useMessageEvent, actions …
|
||||||
|
return { chatStyleId, /* actions */ };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSessionInfo = () => {
|
||||||
|
const shared = useBetween(useSessionInfoState);
|
||||||
|
const userData = useUserDataSnapshot(); // outside useBetween → ok
|
||||||
|
return { ...shared, userFigure: userData.figure, /* etc */ };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Regression guard: `src/hooks/session/useSessionSnapshots.test.tsx`
|
||||||
|
asserts the negative case (snapshot inside useBetween crashes via
|
||||||
|
ErrorBoundary) and the positive case (outside works). A CI gate
|
||||||
|
(`yarn lint:hooks` →
|
||||||
|
`react-hooks/rules-of-hooks: error`) blocks any future commit that
|
||||||
|
reintroduces hook-order issues.
|
||||||
|
|
||||||
### `useNitroEventState` / `useMessageEventState`
|
### `useNitroEventState` / `useMessageEventState`
|
||||||
|
|
||||||
@@ -294,7 +332,7 @@ into `configurePreviewServer` so `yarn preview` keeps working.
|
|||||||
|
|
||||||
| Adopted | Pilot sites |
|
| Adopted | Pilot sites |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Renderer snapshot consumer hooks (`useSessionSnapshots`) | **No in-tree consumers** — three pilot migrations rolled back in `e142efd` due to a runtime `(intermediate value)() is undefined` at `ToolbarView.tsx:46` that survived both the vite-alias fix and defensive guards. The 8 hooks (userData / activeRoomSession / ignoredUsers / groupBadges / soundVolumes / roomUserList / isUserIgnored / groupBadge) remain available as opt-in API with frozen-default fallbacks. |
|
| Renderer snapshot consumer hooks (`useSessionSnapshots`) | `useSessionInfo` (userFigure / userRespectRemaining / petRespectRemaining via `useUserDataSnapshot` in the outer wrapper, outside useBetween), `useChatWidget.ownUserId` (via `useUserDataSnapshot`), `AvatarInfoWidgetAvatarView` Ignore/Unignore (via `useIsUserIgnored`). The 8 hooks (userData / activeRoomSession / ignoredUsers / groupBadges / soundVolumes / roomUserList / isUserIgnored / groupBadge) keep their typeof-guard defensive fallbacks for stale-renderer paths. |
|
||||||
| `useNitroEventState` + companions (Reducer, ExternalSnapshot) | `OfferView`, `useAvatarInfoWidget` (figure/badges/group reducer), `useInventoryFurni` (pure reducers + fragments useRef) |
|
| `useNitroEventState` + companions (Reducer, ExternalSnapshot) | `OfferView`, `useAvatarInfoWidget` (figure/badges/group reducer), `useInventoryFurni` (pure reducers + fragments useRef) |
|
||||||
| `useNitroQuery` + `useNitroEventInvalidator` | `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, `CfhChatlogView`, `useGiftConfiguration`, `useUserGroups`, `useClubOffers(windowId)`, `useSellablePetPalette(breed)`, `useMarketplaceConfiguration`, `useClubGifts` (with invalidator) |
|
| `useNitroQuery` + `useNitroEventInvalidator` | `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, `CfhChatlogView`, `useGiftConfiguration`, `useUserGroups`, `useClubOffers(windowId)`, `useSellablePetPalette(breed)`, `useMarketplaceConfiguration`, `useClubGifts` (with invalidator) |
|
||||||
| Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`), `WiredCreatorToolsView` (`useWiredCreatorToolsUiStore` — every panel-lifecycle-relevant flag, snapshot, selection, highlight, inline editor, picker chain hoisted; what's left in the component as `useState` is genuinely transient: keepSelected, globalClock, roomEnteredAt, selectedMonitorErrorType, selectedMonitorLogDetails) |
|
| Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`), `WiredCreatorToolsView` (`useWiredCreatorToolsUiStore` — every panel-lifecycle-relevant flag, snapshot, selection, highlight, inline editor, picker chain hoisted; what's left in the component as `useState` is genuinely transient: keepSelected, globalClock, roomEnteredAt, selectedMonitorErrorType, selectedMonitorLogDetails) |
|
||||||
@@ -307,9 +345,9 @@ into `configurePreviewServer` so `yarn preview` keeps working.
|
|||||||
|
|
||||||
| Not yet | Notes |
|
| Not yet | Notes |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Split `useChatWidget` / `useAvatarInfoWidget` (data/actions) | Both state-driven via events with no clean imperative actions to extract — split still skip-motivated, but `useAvatarInfoWidget` got a typed `__nitroAvatarClickControl` accessor + module-scope DEBOUNCE const in 2026-05-18 (commit `05ff7df`). The `useChatWidget` reactive-`ownUserId` migration in the same commit was rolled back in `e142efd`; the hook is back on `GetSessionDataManager()?.userId` (static at mount). |
|
| Split `useChatWidget` / `useAvatarInfoWidget` (data/actions) | Both state-driven via events with no clean imperative actions to extract — split still skip-motivated, but `useAvatarInfoWidget` got a typed `__nitroAvatarClickControl` accessor + module-scope DEBOUNCE const in 2026-05-18 (commit `05ff7df`). `useChatWidget.ownUserId` reactive migration re-applied 2026-05-19 in `d28819d` via `useUserDataSnapshot` (direct hook call — useChatWidget isn't wrapped in useBetween so the snapshot-outside-useBetween constraint doesn't apply). |
|
||||||
| Split `usePetPackageWidget` / `useWordQuizWidget` / `useChatCommandSelector` (data/actions) | Data/actions split remains a bad fit, but all three got real modernization in 2026-05-18 instead: usePetPackageWidget → useReducer + extracted `getPetPackageNameError` pure helper + 4 tests; useWordQuizWidget → fixed stale-closure bug in `setUserAnswers` updater + `useRef` for the timeout handle; useChatCommandSelector → module-level `let` cache replaced with a Zustand store. |
|
| Split `usePetPackageWidget` / `useWordQuizWidget` / `useChatCommandSelector` (data/actions) | Data/actions split remains a bad fit, but all three got real modernization in 2026-05-18 instead: usePetPackageWidget → useReducer + extracted `getPetPackageNameError` pure helper + 4 tests; useWordQuizWidget → fixed stale-closure bug in `setUserAnswers` updater + `useRef` for the timeout handle; useChatCommandSelector → module-level `let` cache replaced with a Zustand store. |
|
||||||
| Migrate any consumer to renderer snapshot hooks | **Blocked.** Three pilot migrations were rolled back in `e142efd` after the `useSessionInfo` migration triggered a persistent runtime error at `ToolbarView.tsx:46`. The defensive guards in `useSessionSnapshots.ts` and the umbrella vite alias (`790ad2b`) are still in place. Before retrying, isolate the cause: the suspected interaction is `useBetween` + `useSyncExternalStore` + React Compiler. Try a NON-`useBetween` consumer first (e.g. a fresh per-component hook usage in a low-blast-radius widget). |
|
| Migrate more consumers to renderer snapshot hooks | **Unblocked.** Three pilot consumers shipped 2026-05-19 (`d28819d`), pattern documented above. Next candidates: any code reading from `GetSessionDataManager().userId / userName / clubLevel / securityLevel`, `GetRoomSessionManager().getActiveSession()`, or `GetSoundManager().<volume>` synchronously — those don't re-render today when the value changes. Rule: snapshot read MUST be outside any `useBetween` scope (CI gate `yarn lint:hooks` catches violations; regression test at `src/hooks/session/useSessionSnapshots.test.tsx`). |
|
||||||
| Widen the component / hook test coverage | Mock layer is in place (`src/nitro-renderer.mock.ts`) and 3+ hook/component pilots pass. Good follow-up targets: `LoginView` Form Actions happy/error paths, `OfferView` with `useNitroQuery`. (Acceptable only as a side-effect of a real change — coverage growth on its own is deprioritized per session feedback.) |
|
| Widen the component / hook test coverage | Mock layer is in place (`src/nitro-renderer.mock.ts`) and 3+ hook/component pilots pass. Good follow-up targets: `LoginView` Form Actions happy/error paths, `OfferView` with `useNitroQuery`. (Acceptable only as a side-effect of a real change — coverage growth on its own is deprioritized per session feedback.) |
|
||||||
|
|
||||||
## Known open logic bugs
|
## Known open logic bugs
|
||||||
|
|||||||
+30
-9
@@ -109,15 +109,36 @@ information when forced into a single selector.
|
|||||||
derivations `useIsUserIgnored`, `useGroupBadge`), each with defensive
|
derivations `useIsUserIgnored`, `useGroupBadge`), each with defensive
|
||||||
`typeof` guards against a stale renderer bundle.
|
`typeof` guards against a stale renderer bundle.
|
||||||
|
|
||||||
**Note (2026-05-18):** the first three pilot migrations (`useSessionInfo`,
|
**Hard constraint — snapshot hooks must run outside `useBetween`.**
|
||||||
`useChatWidget.ownUserId`, `AvatarInfoWidgetAvatarView` Ignore-menu)
|
`use-between` 1.x swaps the React dispatcher with its own proxy
|
||||||
were rolled back in `e142efd` after a persistent runtime error
|
(`ownDispatcher` at
|
||||||
`(intermediate value)() is undefined` at `ToolbarView.tsx:46` that
|
`node_modules/use-between/release/index.esm.js:54-169`) that
|
||||||
the vite-alias fix (`790ad2b`) and defensive guards (`c35a2d4`) could
|
reimplements only useState / useReducer / useEffect /
|
||||||
not eliminate. Suspected interaction: `useBetween` +
|
useLayoutEffect / useCallback / useMemo / useRef /
|
||||||
`useSyncExternalStore` + React Compiler. Before retrying any
|
useImperativeHandle. `useSyncExternalStore` is not on the list, so
|
||||||
migration here, exercise the snapshot hooks from a non-`useBetween`
|
calling a snapshot hook inside `useBetween(stateFn)` invokes
|
||||||
consumer in a low-blast-radius widget first to isolate the cause.
|
`undefined(...)` and crashes the first render with
|
||||||
|
"(intermediate value)() is undefined" (Firefox) /
|
||||||
|
"dispatcher.useSyncExternalStore is not a function" (Chrome). This
|
||||||
|
is what blocked the original 2026-05-18 migration of
|
||||||
|
`useSessionInfo` — the rollback (`e142efd`) was correct as a stop
|
||||||
|
the bleed, but neither the vite alias (`790ad2b`) nor the
|
||||||
|
defensive renderer-method guards (`c35a2d4`) could address it
|
||||||
|
because both were downstream of the dispatcher proxy.
|
||||||
|
|
||||||
|
**Fix landed 2026-05-19 (`d28819d`).** Three pilot consumers shipped:
|
||||||
|
`useSessionInfo` (snapshot read in the outer wrapper, after
|
||||||
|
`useBetween`); `useChatWidget.ownUserId` (direct hook call —
|
||||||
|
`useChatWidget` is not wrapped in `useBetween`);
|
||||||
|
`AvatarInfoWidgetAvatarView` Ignore/Unignore (direct hook call in a
|
||||||
|
component body via `useIsUserIgnored`). Pattern documented in
|
||||||
|
`CLAUDE.md` under "Patterns to use →
|
||||||
|
`useSessionSnapshots`". Regression guard:
|
||||||
|
`src/hooks/session/useSessionSnapshots.test.tsx` (negative case via
|
||||||
|
`ErrorBoundary` + positive case). CI gate:
|
||||||
|
`yarn lint:hooks` (`eslint.hooks.config.mjs` →
|
||||||
|
`react-hooks/rules-of-hooks: error`) wired into
|
||||||
|
`.github/workflows/ci.yml`.
|
||||||
|
|
||||||
For state owned outside the listener (the `useState` + `setState(prev =>
|
For state owned outside the listener (the `useState` + `setState(prev =>
|
||||||
applyX(prev, event))` pattern), keep using `useNitroEvent` /
|
applyX(prev, event))` pattern), keep using `useNitroEvent` /
|
||||||
|
|||||||
Reference in New Issue
Block a user