Merge pull request #238 from simoleo89/fix/inventory-state-bugs

fix(inventory): state-mutation & stale-read bugs
This commit is contained in:
DuckieTM
2026-06-15 07:14:28 +02:00
committed by GitHub
3 changed files with 39 additions and 39 deletions
+7 -11
View File
@@ -1,5 +1,5 @@
import { BadgeReceivedEvent, BadgesEvent, RequestBadgesComposer, SetActivatedBadgesComposer } from '@nitrots/nitro-renderer'; import { BadgeReceivedEvent, BadgesEvent, RequestBadgesComposer, SetActivatedBadgesComposer } from '@nitrots/nitro-renderer';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useState } from 'react';
import { useBetween } from 'use-between'; import { useBetween } from 'use-between';
import { GetConfigurationValue, SendMessageComposer, UnseenItemCategory } from '../../api'; import { GetConfigurationValue, SendMessageComposer, UnseenItemCategory } from '../../api';
import { useMessageEvent } from '../events'; import { useMessageEvent } from '../events';
@@ -17,7 +17,6 @@ const useInventoryBadgesState = () =>
const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker(); const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker();
const maxBadgeCount = GetConfigurationValue<number>('user.badges.max.slots', 5); const maxBadgeCount = GetConfigurationValue<number>('user.badges.max.slots', 5);
const pendingUpdatesRef = useRef(0);
const isWearingBadge = (badgeCode: string) => activeBadgeCodes.some(code => code === badgeCode); const isWearingBadge = (badgeCode: string) => activeBadgeCodes.some(code => code === badgeCode);
const canWearBadges = () => (activeBadgeCodes.filter(Boolean).length < maxBadgeCount); const canWearBadges = () => (activeBadgeCodes.filter(Boolean).length < maxBadgeCount);
@@ -35,7 +34,6 @@ const useInventoryBadgesState = () =>
const sendActiveBadges = (badges: (string | null)[]) => const sendActiveBadges = (badges: (string | null)[]) =>
{ {
pendingUpdatesRef.current++;
const composer = new SetActivatedBadgesComposer(); const composer = new SetActivatedBadgesComposer();
for(let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(badges[i] ?? ''); for(let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(badges[i] ?? '');
SendMessageComposer(composer); SendMessageComposer(composer);
@@ -93,16 +91,14 @@ const useInventoryBadgesState = () =>
return newValue; return newValue;
}); });
// Skip overwriting activeBadgeCodes if we have pending local changes // The emulator answers SetActivatedBadges (UserWearBadgeEvent) with a
if(pendingUpdatesRef.current > 0) // UserBadgesComposer room broadcast, NOT a BadgesEvent — so there is no
{ // echo to suppress and the old pendingUpdatesRef counter only ever
pendingUpdatesRef.current--; // leaked (incremented on every edit, never decremented), which then
} // silently dropped legitimate later BadgesEvent updates. The server is
else // authoritative here (edits are already persisted), so always apply it.
{
const serverBadges = parser.getActiveBadgeCodes(); const serverBadges = parser.getActiveBadgeCodes();
setActiveBadgeCodes(toFixedSlots(serverBadges)); setActiveBadgeCodes(toFixedSlots(serverBadges));
}
setBadgeCodes(allBadgeCodes); setBadgeCodes(allBadgeCodes);
}); });
+12 -8
View File
@@ -78,25 +78,29 @@ const useInventoryPrefixesState = () =>
setPrefixes(prevValue => setPrefixes(prevValue =>
{ {
return prevValue.map(p => ({ const next = prevValue.map(p => ({
...p, ...p,
active: p.id === parser.prefixId active: p.id === parser.prefixId
})); }));
});
// Derive the active prefix from the freshly-mapped list, not from
// the `prefixes` closure (which lags a render and is stale when the
// prefix was added earlier in the same event batch).
if(parser.prefixId === 0) if(parser.prefixId === 0)
{ {
setActivePrefix(null); setActivePrefix(null);
} }
else else
{ {
setActivePrefix(prev => const found = next.find(p => p.id === parser.prefixId);
{
const found = prefixes.find(p => p.id === parser.prefixId); setActivePrefix(found
if(found) return { ...found, active: true, font: parser.font || found.font || '' }; ? { ...found, active: true, font: parser.font || found.font || '' }
return { id: parser.prefixId, text: parser.text, color: parser.color, icon: parser.icon || '', effect: parser.effect || '', font: parser.font || '', active: true }; : { id: parser.prefixId, text: parser.text, color: parser.color, icon: parser.icon || '', effect: parser.effect || '', font: parser.font || '', active: true });
});
} }
return next;
});
}); });
const activatePrefix = (prefixId: number) => const activatePrefix = (prefixId: number) =>
@@ -63,7 +63,10 @@ const useInventoryUnseenTrackerState = () =>
const newValue = new Map(prevValue); const newValue = new Map(prevValue);
const existing = newValue.get(category); const existing = newValue.get(category);
if(existing) for(const itemId of itemIds) existing.splice(existing.indexOf(itemId), 1); // Replace the per-category array instead of splicing the one still
// referenced by the previous Map, and filter (an absent id used to
// splice(indexOf=-1) and drop the wrong last element).
if(existing) newValue.set(category, existing.filter(id => !itemIds.includes(id)));
sendResetItemsMessage(category, itemIds); sendResetItemsMessage(category, itemIds);
@@ -90,9 +93,9 @@ const useInventoryUnseenTrackerState = () =>
const newValue = new Map(prevValue); const newValue = new Map(prevValue);
const items = newValue.get(category); const items = newValue.get(category);
const index = items.indexOf(itemId);
if(index >= 0) items.splice(index, 1); // Clone the array rather than splicing the one shared with prevValue.
if(items && items.indexOf(itemId) >= 0) newValue.set(category, items.filter(id => id !== itemId));
return newValue; return newValue;
}); });
@@ -108,18 +111,15 @@ const useInventoryUnseenTrackerState = () =>
for(const category of parser.categories) for(const category of parser.categories)
{ {
let existing = newValue.get(category); // Clone the existing array so we never push into the one still
// referenced by the previous (shallow-copied) Map.
if(!existing) const merged = [ ...(newValue.get(category) ?? []) ];
{
existing = [];
newValue.set(category, existing);
}
const itemIds = parser.getItemsByCategory(category); const itemIds = parser.getItemsByCategory(category);
for(const itemId of itemIds) ((existing.indexOf(itemId) === -1) && existing.push(itemId)); for(const itemId of itemIds) if(merged.indexOf(itemId) === -1) merged.push(itemId);
newValue.set(category, merged);
} }
return newValue; return newValue;