Fix inventory window never refreshing live (per-character version)

The inventoryVersion counter in useLiveData was a single global value that
bumped on every inventory_delta for any character. With 60+ active chars
all generating deltas, the global counter advanced multiple times per
second.

InventoryWindow's debounce effect watched this global counter, so every
bump reset its 2-second fetch timer. Since bumps arrived faster than 2s,
the fetch never fired — the window appeared frozen until the user closed
and reopened it (which triggered the initial-fetch effect).

Fix: make inventoryVersions a Map<string, number> keyed by character name.
Each inventory_delta now only bumps its own character's counter, so an
open window's debounce correctly fires 2s after its character's last
delta, ignoring unrelated traffic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-15 18:57:55 +02:00
parent 7dc5996820
commit d26f1f725c
18 changed files with 176 additions and 9 deletions

View file

@ -66,7 +66,7 @@ export const MapLayout: React.FC<Props> = ({ data }) => {
selectedPlayer={selectedPlayer}
/>
<WindowRenderer characters={data.characters} chatMessages={data.chatMessages}
nearbyObjects={data.nearbyObjects} inventoryVersion={data.inventoryVersion}
nearbyObjects={data.nearbyObjects} inventoryVersions={data.inventoryVersions}
equipmentCantrips={data.equipmentCantrips} characterStats={data.characterStats}
socket={data.socketRef.current} />
<RareNotification recentRares={data.recentRares} />

View file

@ -17,13 +17,15 @@ interface Props {
characters: Map<string, CharacterState>;
chatMessages: Map<string, Array<{ text: string; color?: number; timestamp: string }>>;
nearbyObjects: Map<string, any>;
inventoryVersion: number;
/** Per-character inventory counters. InventoryWindow watches only its
* own character's value so unrelated deltas don't reset its debounce. */
inventoryVersions: Map<string, number>;
equipmentCantrips: Map<string, any>;
characterStats: Map<string, any>;
socket: WebSocket | null;
}
export const WindowRenderer: React.FC<Props> = React.memo(({ characters, chatMessages, nearbyObjects, inventoryVersion, equipmentCantrips, characterStats, socket }) => {
export const WindowRenderer: React.FC<Props> = React.memo(({ characters, chatMessages, nearbyObjects, inventoryVersions, equipmentCantrips, characterStats, socket }) => {
const { windows } = useWindowManager();
return (
@ -44,7 +46,7 @@ export const WindowRenderer: React.FC<Props> = React.memo(({ characters, chatMes
liveStats={characterStats.get(charName)} />;
case 'inv':
return <InventoryWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex}
inventoryVersion={inventoryVersion} equipmentCantrips={equipmentCantrips.get(charName)} />;
inventoryVersion={inventoryVersions.get(charName) ?? 0} equipmentCantrips={equipmentCantrips.get(charName)} />;
case 'radar':
return <RadarWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex}
socket={socket} radarData={nearbyObjects.get(charName) ?? null} />;

View file

@ -14,7 +14,11 @@ export interface DashboardState {
recentRares: RareMessage[];
chatMessages: Map<string, Array<{ text: string; color?: number; timestamp: string }>>;
nearbyObjects: Map<string, any>;
inventoryVersion: number;
/** Per-character inventory version counter bumps when that character
* receives an inventory_delta. Open windows watch only their own
* character's counter so deltas for unrelated chars don't reset their
* debounce timer. */
inventoryVersions: Map<string, number>;
equipmentCantrips: Map<string, any>;
characterStats: Map<string, any>;
deathAlerts: Array<{ character_name: string; vitae: number; timestamp: string }>;
@ -29,7 +33,7 @@ export function useLiveData(): DashboardState {
const [recentRares, setRecentRares] = useState<RareMessage[]>([]);
const chatMessagesRef = useRef(new Map<string, Array<{ text: string; color?: number; timestamp: string }>>());
const [chatVersion, setChatVersion] = useState(0);
const [inventoryVersion, setInventoryVersion] = useState(0);
const [inventoryVersions, setInventoryVersions] = useState<Map<string, number>>(new Map());
const equipmentCantripRef = useRef(new Map<string, any>());
const [equipCantripVersion, setEquipCantripVersion] = useState(0);
const characterStatsRef = useRef(new Map<string, any>());
@ -72,8 +76,17 @@ export function useLiveData(): DashboardState {
setRecentRares(prev => [r, ...prev].slice(0, 50));
} else if (msg.type === 'inventory_delta') {
const d = msg as unknown as { character_name: string };
// Bump inventory version so open inventory windows can re-fetch
setInventoryVersion(v => v + 1);
// Bump ONLY this character's inventory version so an open window for
// that character re-fetches. Deltas for other characters don't touch
// it, which keeps the 2s debounce in InventoryWindow from being reset
// forever by unrelated chatter.
if (d.character_name) {
setInventoryVersions(prev => {
const next = new Map(prev);
next.set(d.character_name, (next.get(d.character_name) ?? 0) + 1);
return next;
});
}
} else if (msg.type === 'character_stats') {
// Store full character stats for CharacterWindow live updates
const cs = msg as unknown as { character_name: string };
@ -200,5 +213,5 @@ export function useLiveData(): DashboardState {
// eslint-disable-next-line react-hooks/exhaustive-deps
const characterStats = useMemo(() => characterStatsRef.current, [charStatsVersion]);
return { characters, serverHealth, totalRares, totalKills, recentRares, chatMessages, nearbyObjects, inventoryVersion, equipmentCantrips, characterStats, deathAlerts, socketRef };
return { characters, serverHealth, totalRares, totalKills, recentRares, chatMessages, nearbyObjects, inventoryVersions, equipmentCantrips, characterStats, deathAlerts, socketRef };
}