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 { AvailableCommandsEvent, GetCommunication } from '@nitrots/nitro-renderer';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { CommandDefinition } from '../../../api';
|
import { CommandDefinition } from '../../../api';
|
||||||
|
import { createNitroStore } from '../../../state/createNitroStore';
|
||||||
import { useMessageEvent } from '../../events';
|
import { useMessageEvent } from '../../events';
|
||||||
|
|
||||||
|
// Client-only commands are static; safe to keep at module scope.
|
||||||
const CLIENT_COMMANDS: CommandDefinition[] = [
|
const CLIENT_COMMANDS: CommandDefinition[] = [
|
||||||
// Effetti stanza
|
// Effetti stanza
|
||||||
{ key: 'shake', description: 'Scuoti la stanza' },
|
{ key: 'shake', description: 'Scuoti la stanza' },
|
||||||
@@ -31,60 +33,79 @@ const CLIENT_COMMANDS: CommandDefinition[] = [
|
|||||||
{ key: 'nitro', description: 'Info client' },
|
{ key: 'nitro', description: 'Info client' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Module-level cache: cattura i comandi dal server anche prima che React monti
|
/**
|
||||||
let cachedServerCommands: CommandDefinition[] = [];
|
* Server-pushed command cache. Lives in a Zustand store (instead of
|
||||||
let globalListenerRegistered = false;
|
* module-level `let` variables) so the React Compiler can analyze the
|
||||||
|
* surrounding hook cleanly, and so a future test can `setState({…})`
|
||||||
function ensureGlobalListener(): void
|
* 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;
|
serverCommands: CommandDefinition[];
|
||||||
globalListenerRegistered = true;
|
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
|
try
|
||||||
{
|
{
|
||||||
const event = new AvailableCommandsEvent((event: AvailableCommandsEvent) =>
|
const event = new AvailableCommandsEvent((event: AvailableCommandsEvent) =>
|
||||||
{
|
{
|
||||||
const parser = event.getParser();
|
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);
|
GetCommunication().registerMessageEvent(event);
|
||||||
|
useChatCommandStore.getState().markListenerRegistered();
|
||||||
}
|
}
|
||||||
catch(e)
|
catch
|
||||||
{
|
{
|
||||||
// Communication not ready yet, will retry on hook mount
|
// Communication not ready yet — the in-hook useMessageEvent
|
||||||
globalListenerRegistered = false;
|
// 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();
|
ensureGlobalListener();
|
||||||
|
|
||||||
export const useChatCommandSelector = (chatValue: string) =>
|
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 [ selectedIndex, setSelectedIndex ] = useState(0);
|
||||||
const [ dismissed, setDismissed ] = useState(false);
|
const [ dismissed, setDismissed ] = useState(false);
|
||||||
|
|
||||||
// Ensure global listener is registered
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
// Cover the case where the module-level registration failed
|
||||||
|
// because GetCommunication() wasn't ready at import time.
|
||||||
ensureGlobalListener();
|
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 =>
|
useMessageEvent<AvailableCommandsEvent>(AvailableCommandsEvent, event =>
|
||||||
{
|
{
|
||||||
const parser = event.getParser();
|
const parser = event.getParser();
|
||||||
const cmds = parser.commands.map(cmd => ({ key: cmd.key, description: cmd.description }));
|
setServerCommands(parser.commands.map(cmd => ({ key: cmd.key, description: cmd.description })));
|
||||||
cachedServerCommands = cmds;
|
|
||||||
setServerCommands(cmds);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const allCommands = useMemo(() =>
|
const allCommands = useMemo(() =>
|
||||||
@@ -93,10 +114,7 @@ export const useChatCommandSelector = (chatValue: string) =>
|
|||||||
|
|
||||||
for(const clientCmd of CLIENT_COMMANDS)
|
for(const clientCmd of CLIENT_COMMANDS)
|
||||||
{
|
{
|
||||||
if(!merged.some(cmd => cmd.key === clientCmd.key))
|
if(!merged.some(cmd => cmd.key === clientCmd.key)) merged.push(clientCmd);
|
||||||
{
|
|
||||||
merged.push(clientCmd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return merged.sort((a, b) => a.key.localeCompare(b.key));
|
return merged.sort((a, b) => a.key.localeCompare(b.key));
|
||||||
|
|||||||
Reference in New Issue
Block a user