mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
useClubGifts + useNitroEventInvalidator: close the catalogOptions bag
This commit drains the last field out of ICatalogOptions (clubGifts) and deletes the interface — useCatalog no longer owns a catch-all mutable object that downstream components stuff data into. Two pieces: 1) New useNitroEventInvalidator(eventType, queryKey, accept?) — a small companion to useNitroQuery for the case where the server pushes the same event unprompted (e.g. ClubGiftInfoEvent fires both as the response to GetClubGiftInfo and again after the user claims a gift via SelectClubGiftComposer). It calls queryClient.invalidateQueries() on each matching push so the next render of any subscriber triggers a fresh queryFn. 2) New useClubGifts() — useNitroQuery on the ClubGiftInfoEvent pair, paired with useNitroEventInvalidator so server-driven pushes refresh the cache automatically. CatalogLayoutVipGiftsView now consumes the query directly. The local optimistic 'giftsAvailable--' mutation (which side-effected the parser object passed back to the catalog state!) is dropped — the server's authoritative ClubGiftInfoEvent push is the single source of truth via the invalidator. useCatalog drops the matching listener + the GetClubGiftInfo dispatch from the catalog-open effect. ICatalogOptions is now empty and deleted; the catalogOptions / setCatalogOptions state + return-shape field are removed from useCatalog along with the import.
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
import { ClubGiftInfoParser } from '@nitrots/nitro-renderer';
|
||||
|
||||
export interface ICatalogOptions
|
||||
{
|
||||
clubGifts?: ClubGiftInfoParser;
|
||||
}
|
||||
@@ -10,7 +10,6 @@ export * from './FurnitureOffer';
|
||||
export * from './GetImageIconUrlForProduct';
|
||||
export * from './GiftWrappingConfiguration';
|
||||
export * from './ICatalogNode';
|
||||
export * from './ICatalogOptions';
|
||||
export * from './ICatalogPage';
|
||||
export * from './IMarketplaceSearchOptions';
|
||||
export * from './IPageLocalization';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './createNitroQuery';
|
||||
export * from './useNitroEventInvalidator';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { IMessageEvent, MessageEvent } from '@nitrots/nitro-renderer';
|
||||
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMessageEvent } from '../../hooks/events/useMessageEvent';
|
||||
|
||||
/**
|
||||
* Invalidate a TanStack query slot every time the renderer pushes the
|
||||
* matching parser event. Companion to useNitroQuery for the case where
|
||||
* the server can push fresh data unprompted (e.g. ClubGiftInfoEvent
|
||||
* fires both as the response to GetClubGiftInfo and again after the
|
||||
* user claims a gift via SelectClubGiftComposer).
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* const { data: clubGifts } = useNitroQuery({
|
||||
* key: ['nitro', 'catalog', 'clubGifts'],
|
||||
* request: () => new GetClubGiftInfo(),
|
||||
* parser: ClubGiftInfoEvent,
|
||||
* select: e => e.getParser(),
|
||||
* });
|
||||
*
|
||||
* // re-fetch on every server push:
|
||||
* useNitroEventInvalidator(ClubGiftInfoEvent, ['nitro', 'catalog', 'clubGifts']);
|
||||
*
|
||||
* Optional `accept` predicate filters out events that don't belong to
|
||||
* this query slot — useful when the same parser is multiplexed across
|
||||
* multiple correlated queries (mirrors useNitroQuery.accept).
|
||||
*
|
||||
* Implementation: the renderer push triggers `queryClient.invalidateQueries`,
|
||||
* which marks the slot stale; the next subscriber render triggers a
|
||||
* fresh fetch via useNitroQuery's queryFn. If nobody is currently
|
||||
* subscribed, the invalidation is a no-op (TanStack drops stale entries
|
||||
* with no active observers per its garbage-collection policy).
|
||||
*/
|
||||
export const useNitroEventInvalidator = <T extends IMessageEvent>(
|
||||
eventType: typeof MessageEvent,
|
||||
queryKey: QueryKey,
|
||||
accept?: (event: T) => boolean
|
||||
) =>
|
||||
{
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useMessageEvent<T>(eventType, event =>
|
||||
{
|
||||
if(accept && !accept(event)) return;
|
||||
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
});
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { SelectClubGiftComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useMemo } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../../../../api';
|
||||
import { AutoGrid, Text } from '../../../../../../common';
|
||||
import { useCatalog, useNotification, usePurse } from '../../../../../../hooks';
|
||||
import { useClubGifts, useNotification, usePurse } from '../../../../../../hooks';
|
||||
import { CatalogLayoutProps } from '../CatalogLayout.types';
|
||||
import { VipGiftItem } from './VipGiftItemView';
|
||||
|
||||
@@ -11,8 +11,7 @@ let isSelectingGift = false;
|
||||
export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { purse = null } = usePurse();
|
||||
const { catalogOptions = null, setCatalogOptions = null } = useCatalog();
|
||||
const { clubGifts = null } = catalogOptions;
|
||||
const { data: clubGifts = null } = useClubGifts();
|
||||
const { showConfirm = null } = useNotification();
|
||||
|
||||
const giftsAvailable = useCallback(() =>
|
||||
@@ -36,34 +35,32 @@ export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
|
||||
|
||||
isSelectingGift = true;
|
||||
|
||||
// The server replies with a fresh ClubGiftInfoEvent after
|
||||
// accepting the selection; useClubGifts subscribes to that
|
||||
// event via useNitroEventInvalidator, so giftsAvailable
|
||||
// refreshes from the authoritative source — no need to
|
||||
// mutate the parser locally.
|
||||
SendMessageComposer(new SelectClubGiftComposer(localizationId));
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
prevValue.clubGifts.giftsAvailable--;
|
||||
|
||||
return { ...prevValue };
|
||||
});
|
||||
|
||||
setTimeout(() => isSelectingGift = false, 5000);
|
||||
}, null);
|
||||
}, [ setCatalogOptions, showConfirm ]);
|
||||
}, [ showConfirm ]);
|
||||
|
||||
const sortGifts = useMemo(() =>
|
||||
{
|
||||
let gifts = clubGifts.offers.sort((a,b) =>
|
||||
{
|
||||
return clubGifts.getOfferExtraData(a.offerId).daysRequired - clubGifts.getOfferExtraData(b.offerId).daysRequired;
|
||||
});
|
||||
return gifts;
|
||||
},[ clubGifts ]);
|
||||
if(!clubGifts) return [];
|
||||
|
||||
return [ ...clubGifts.offers ].sort((a, b) =>
|
||||
(clubGifts.getOfferExtraData(a.offerId).daysRequired - clubGifts.getOfferExtraData(b.offerId).daysRequired)
|
||||
);
|
||||
}, [ clubGifts ]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text shrink truncate fontWeight="bold">{ giftsAvailable() }</Text>
|
||||
<AutoGrid className="nitro-catalog-layout-vip-gifts-grid" columnCount={ 1 }>
|
||||
{ (clubGifts.offers.length > 0) && sortGifts.map(offer => <VipGiftItem key={ offer.offerId } daysRequired={ clubGifts.getOfferExtraData(offer.offerId).daysRequired } isAvailable={ (clubGifts.getOfferExtraData(offer.offerId).isSelectable && (clubGifts.giftsAvailable > 0)) } offer={ offer } onSelect={ selectGift }/>) }
|
||||
{ clubGifts && (clubGifts.offers.length > 0) && sortGifts.map(offer => <VipGiftItem key={ offer.offerId } daysRequired={ clubGifts.getOfferExtraData(offer.offerId).daysRequired } isAvailable={ (clubGifts.getOfferExtraData(offer.offerId).isSelectable && (clubGifts.giftsAvailable > 0)) } offer={ offer } onSelect={ selectGift }/>) }
|
||||
</AutoGrid>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from './useCatalog';
|
||||
export * from './useCatalogFavorites';
|
||||
export * from './useCatalogPlaceMultipleItems';
|
||||
export * from './useCatalogSkipPurchaseConfirmation';
|
||||
export * from './useClubGifts';
|
||||
export * from './useClubOffers';
|
||||
export * from './useGiftConfiguration';
|
||||
export * from './useMarketplaceConfiguration';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, ClubGiftInfoEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetClubGiftInfo, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, NodeData, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomControllerLevel, RoomEngineObjectPlacedEvent, RoomObjectCategory, RoomObjectPlacementSource, RoomObjectType, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, NodeData, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomControllerLevel, RoomEngineObjectPlacedEvent, RoomObjectCategory, RoomObjectPlacementSource, RoomObjectType, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { BuilderFurniPlaceableStatus, CatalogNode, CatalogPage, CatalogType, DispatchUiEvent, FurniCategory, GetFurnitureData, GetProductDataForLocalization, GetRoomSession, ICatalogNode, ICatalogOptions, ICatalogPage, IPageLocalization, IProduct, IPurchasableOffer, IPurchaseOptions, LocalizeText, NotificationAlertType, Offer, PageLocalization, PlacedObjectPurchaseData, PlaySound, Product, ProductTypeEnum, RequestedPage, SearchResult, SendMessageComposer, SoundNames } from '../../api';
|
||||
import { BuilderFurniPlaceableStatus, CatalogNode, CatalogPage, CatalogType, DispatchUiEvent, FurniCategory, GetFurnitureData, GetProductDataForLocalization, GetRoomSession, ICatalogNode, ICatalogPage, IPageLocalization, IProduct, IPurchasableOffer, IPurchaseOptions, LocalizeText, NotificationAlertType, Offer, PageLocalization, PlacedObjectPurchaseData, PlaySound, Product, ProductTypeEnum, RequestedPage, SearchResult, SendMessageComposer, SoundNames } from '../../api';
|
||||
import { CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogPurchaseSoldOutEvent, CatalogPurchasedEvent, InventoryFurniAddedEvent } from '../../events';
|
||||
import { useMessageEvent, useNitroEvent, useUiEvent } from '../events';
|
||||
import { useNotification } from '../notification';
|
||||
@@ -28,7 +28,6 @@ const useCatalogState = () =>
|
||||
const [ roomPreviewer, setRoomPreviewer ] = useState<RoomPreviewer>(null);
|
||||
const [ navigationHidden, setNavigationHidden ] = useState(false);
|
||||
const [ purchaseOptions, setPurchaseOptions ] = useState<IPurchaseOptions>({ quantity: 1, extraData: null, extraParamRequired: false, previewStuffData: null });
|
||||
const [ catalogOptions, setCatalogOptions ] = useState<ICatalogOptions>({});
|
||||
const [ objectMoverRequested, setObjectMoverRequested ] = useState(false);
|
||||
const [ catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects ] = useCatalogPlaceMultipleItems();
|
||||
const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation();
|
||||
@@ -734,18 +733,6 @@ const useCatalogState = () =>
|
||||
simpleAlert(message, NotificationAlertType.DEFAULT, null, null, title);
|
||||
});
|
||||
|
||||
useMessageEvent<ClubGiftInfoEvent>(ClubGiftInfoEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
const clubGifts = parser;
|
||||
|
||||
return { ...prevValue, clubGifts };
|
||||
});
|
||||
});
|
||||
|
||||
useMessageEvent<CatalogPublishedMessageEvent>(CatalogPublishedMessageEvent, event =>
|
||||
{
|
||||
const wasVisible = isVisible;
|
||||
@@ -1024,7 +1011,6 @@ const useCatalogState = () =>
|
||||
{
|
||||
if(!isVisible || rootNode) return;
|
||||
|
||||
SendMessageComposer(new GetClubGiftInfo());
|
||||
SendMessageComposer(new GetCatalogIndexComposer(currentType));
|
||||
SendMessageComposer(new BuildersClubQueryFurniCountMessageComposer());
|
||||
}, [ isVisible, rootNode, currentType ]);
|
||||
@@ -1044,7 +1030,7 @@ const useCatalogState = () =>
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { isVisible, setIsVisible, isBusy, pageId, previousPageId, currentType, rootNode, offersToNodes, currentPage, setCurrentPage, currentOffer, setCurrentOffer, activeNodes, searchResult, setSearchResult, frontPageItems, roomPreviewer, navigationHidden, setNavigationHidden, purchaseOptions, setPurchaseOptions, catalogOptions, setCatalogOptions, catalogLocalizationVersion, getNodeById, getNodeByName, activateNode, openPageById, openPageByName, openPageByOfferId, requestOfferToMover, openCatalogByType, toggleCatalogByType, furniCount, furniLimit, maxFurniLimit, secondsLeft, secondsLeftWithGrace, updateTime, catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects, getBuilderFurniPlaceableStatus, selectCatalogOffer };
|
||||
return { isVisible, setIsVisible, isBusy, pageId, previousPageId, currentType, rootNode, offersToNodes, currentPage, setCurrentPage, currentOffer, setCurrentOffer, activeNodes, searchResult, setSearchResult, frontPageItems, roomPreviewer, navigationHidden, setNavigationHidden, purchaseOptions, setPurchaseOptions, catalogLocalizationVersion, getNodeById, getNodeByName, activateNode, openPageById, openPageByName, openPageByOfferId, requestOfferToMover, openCatalogByType, toggleCatalogByType, furniCount, furniLimit, maxFurniLimit, secondsLeft, secondsLeftWithGrace, updateTime, catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects, getBuilderFurniPlaceableStatus, selectCatalogOffer };
|
||||
};
|
||||
|
||||
export const useCatalog = () => useBetween(useCatalogState);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ClubGiftInfoEvent, ClubGiftInfoParser, GetClubGiftInfo } from '@nitrots/nitro-renderer';
|
||||
import { UseQueryResult } from '@tanstack/react-query';
|
||||
import { useNitroEventInvalidator, useNitroQuery } from '../../api/nitro-query';
|
||||
|
||||
const CLUB_GIFTS_KEY = [ 'nitro', 'catalog', 'clubGifts' ] as const;
|
||||
|
||||
/**
|
||||
* Habbo Club gift availability (counts of pending gifts, days until
|
||||
* next gift, gift list). The server replies once to GetClubGiftInfo
|
||||
* and then pushes ClubGiftInfoEvent again every time the user claims
|
||||
* a gift via SelectClubGiftComposer — so the cache needs to be
|
||||
* invalidated on each push, not just hydrated by the first response.
|
||||
*
|
||||
* Pair the query with useNitroEventInvalidator so unsolicited pushes
|
||||
* mark the slot stale; the next render of any consumer triggers a
|
||||
* re-fetch (which, since the server just pushed, will resolve almost
|
||||
* immediately with the fresh data the server already sent us).
|
||||
*
|
||||
* Replaces the previous useCatalog listener that stuffed
|
||||
* `parser` into `catalogOptions.clubGifts`.
|
||||
*/
|
||||
export const useClubGifts = (
|
||||
options: { enabled?: boolean } = {}
|
||||
): UseQueryResult<ClubGiftInfoParser> =>
|
||||
{
|
||||
const query = useNitroQuery<ClubGiftInfoEvent, ClubGiftInfoParser>({
|
||||
key: CLUB_GIFTS_KEY as unknown as string[],
|
||||
request: () => new GetClubGiftInfo(),
|
||||
parser: ClubGiftInfoEvent,
|
||||
select: event => event.getParser(),
|
||||
enabled: options.enabled,
|
||||
staleTime: Infinity
|
||||
});
|
||||
|
||||
useNitroEventInvalidator<ClubGiftInfoEvent>(ClubGiftInfoEvent, CLUB_GIFTS_KEY as unknown as string[]);
|
||||
|
||||
return query;
|
||||
};
|
||||
Reference in New Issue
Block a user