mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
refactor(useChatCommandSelector): move module-level mutable cache into a Zustand store
Two module-level `let` declarations (cachedServerCommands +
globalListenerRegistered) were tracking the AvailableCommandsEvent
listener state outside React. The pattern was a React Compiler
violation flagged elsewhere in the codebase (the navigatorRoomCreator
fix was the canonical precedent — see commit fd1835c).
Move both into a per-hook Zustand store
(`useChatCommandStore`) following the same convention as
`useWiredCreatorToolsUiStore` and `useRoomCreatorStore`. The store
keeps the cached server-pushed CommandDefinitions plus a
single-shot isListenerRegistered flag that prevents the in-hook
useMessageEvent and the module-level pre-mount listener from
double-registering.
`CLIENT_COMMANDS` stays at module scope — it's a const array,
React Compiler is fine with constant data.
Behavioural change: zero. The pre-mount registration still tries
once at module load (covering the case where the server's
AvailableCommands lands before any React widget mounts). The in-hook
useMessageEvent still covers later mounts and rank-change refreshes.
Every push goes through `setServerCommands`, so all consumers see
the same data.
Side benefit: a future test can now `useChatCommandStore.setState({
serverCommands: [...], isListenerRegistered: true })` to seed a
deterministic fixture without monkey-patching the module.
Public API of useChatCommandSelector unchanged; the one consumer
(ChatInputView) reads the same destructured fields. Verified via grep.
Suite: 207/207.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { AvailableCommandsEvent, GetCommunication } from '@nitrots/nitro-renderer';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { CommandDefinition } from '../../../api';
|
||||
import { createNitroStore } from '../../../state/createNitroStore';
|
||||
import { useMessageEvent } from '../../events';
|
||||
|
||||
// Client-only commands are static; safe to keep at module scope.
|
||||
const CLIENT_COMMANDS: CommandDefinition[] = [
|
||||
// Effetti stanza
|
||||
{ key: 'shake', description: 'Scuoti la stanza' },
|
||||
@@ -31,60 +33,79 @@ const CLIENT_COMMANDS: CommandDefinition[] = [
|
||||
{ key: 'nitro', description: 'Info client' },
|
||||
];
|
||||
|
||||
// Module-level cache: cattura i comandi dal server anche prima che React monti
|
||||
let cachedServerCommands: CommandDefinition[] = [];
|
||||
let globalListenerRegistered = false;
|
||||
|
||||
function ensureGlobalListener(): void
|
||||
/**
|
||||
* Server-pushed command cache. Lives in a Zustand store (instead of
|
||||
* module-level `let` variables) so the React Compiler can analyze the
|
||||
* surrounding hook cleanly, and so a future test can `setState({…})`
|
||||
* a deterministic fixture without monkey-patching the module.
|
||||
*
|
||||
* The `isListenerRegistered` flag prevents the renderer from getting
|
||||
* two AvailableCommandsEvent listeners — one from the module-level
|
||||
* pre-mount registration (which captures the server's reply that lands
|
||||
* during login, BEFORE any React widget mounts) and one from the
|
||||
* in-hook `useMessageEvent` (which covers later rank-change refreshes).
|
||||
*/
|
||||
interface ChatCommandStore
|
||||
{
|
||||
if(globalListenerRegistered) return;
|
||||
globalListenerRegistered = true;
|
||||
serverCommands: CommandDefinition[];
|
||||
isListenerRegistered: boolean;
|
||||
setServerCommands: (commands: CommandDefinition[]) => void;
|
||||
markListenerRegistered: () => void;
|
||||
}
|
||||
|
||||
const useChatCommandStore = createNitroStore<ChatCommandStore>()((set) => ({
|
||||
serverCommands: [],
|
||||
isListenerRegistered: false,
|
||||
setServerCommands: (commands) => set({ serverCommands: commands }),
|
||||
markListenerRegistered: () => set({ isListenerRegistered: true })
|
||||
}));
|
||||
|
||||
const ensureGlobalListener = (): void =>
|
||||
{
|
||||
if(useChatCommandStore.getState().isListenerRegistered) return;
|
||||
|
||||
try
|
||||
{
|
||||
const event = new AvailableCommandsEvent((event: AvailableCommandsEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
cachedServerCommands = parser.commands.map(cmd => ({ key: cmd.key, description: cmd.description }));
|
||||
useChatCommandStore.getState().setServerCommands(parser.commands.map(cmd => ({ key: cmd.key, description: cmd.description })));
|
||||
});
|
||||
|
||||
GetCommunication().registerMessageEvent(event);
|
||||
useChatCommandStore.getState().markListenerRegistered();
|
||||
}
|
||||
catch(e)
|
||||
catch
|
||||
{
|
||||
// Communication not ready yet, will retry on hook mount
|
||||
globalListenerRegistered = false;
|
||||
// Communication not ready yet — the in-hook useMessageEvent
|
||||
// below covers later mounts.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Try to register immediately at module load
|
||||
// Try once at module load so the server's response landing before any
|
||||
// React mount still hits the cache.
|
||||
ensureGlobalListener();
|
||||
|
||||
export const useChatCommandSelector = (chatValue: string) =>
|
||||
{
|
||||
const [ serverCommands, setServerCommands ] = useState<CommandDefinition[]>(cachedServerCommands);
|
||||
const serverCommands = useChatCommandStore(s => s.serverCommands);
|
||||
const setServerCommands = useChatCommandStore(s => s.setServerCommands);
|
||||
const [ selectedIndex, setSelectedIndex ] = useState(0);
|
||||
const [ dismissed, setDismissed ] = useState(false);
|
||||
|
||||
// Ensure global listener is registered
|
||||
useEffect(() =>
|
||||
{
|
||||
// Cover the case where the module-level registration failed
|
||||
// because GetCommunication() wasn't ready at import time.
|
||||
ensureGlobalListener();
|
||||
|
||||
// If cache already has data (from login), use it
|
||||
if(cachedServerCommands.length > 0 && serverCommands.length === 0)
|
||||
{
|
||||
setServerCommands(cachedServerCommands);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Also listen via React hook for any future updates (e.g. rank change)
|
||||
// Late updates (rank change, etc.) — go through the store so all
|
||||
// consumers see the same data.
|
||||
useMessageEvent<AvailableCommandsEvent>(AvailableCommandsEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
const cmds = parser.commands.map(cmd => ({ key: cmd.key, description: cmd.description }));
|
||||
cachedServerCommands = cmds;
|
||||
setServerCommands(cmds);
|
||||
setServerCommands(parser.commands.map(cmd => ({ key: cmd.key, description: cmd.description })));
|
||||
});
|
||||
|
||||
const allCommands = useMemo(() =>
|
||||
@@ -93,10 +114,7 @@ export const useChatCommandSelector = (chatValue: string) =>
|
||||
|
||||
for(const clientCmd of CLIENT_COMMANDS)
|
||||
{
|
||||
if(!merged.some(cmd => cmd.key === clientCmd.key))
|
||||
{
|
||||
merged.push(clientCmd);
|
||||
}
|
||||
if(!merged.some(cmd => cmd.key === clientCmd.key)) merged.push(clientCmd);
|
||||
}
|
||||
|
||||
return merged.sort((a, b) => a.key.localeCompare(b.key));
|
||||
|
||||
Reference in New Issue
Block a user