useClubOffers: per-windowId TanStack query for HC offer pages

Two catalog layouts each fire 'new GetClubOffersMessageComposer(windowId)'
on mount and read parser.offers via HabboClubOffersMessageEvent:

  - CatalogLayoutVipBuyView (windowId 1)
  - CatalogLayoutBuildersClubBuyView (windowId 2 / 3, depending on
    the addon variant)

Plus useCatalog used to also listen for HabboClubOffersMessageEvent and
stash the offers in 'catalogOptions.clubOffersByWindowId[windowId]' and
'catalogOptions.clubOffers' (the latter being a backward-compat alias
for windowId 1). Three listeners, three independent requests when all
mounted.

New useClubOffers(windowId) wraps the request/response pair as a
TanStack query keyed by '['nitro', 'catalog', 'clubOffers', windowId]'.
accept(): correlation-key filter (parser.windowId === windowId) so
the same multiplexed event doesn't satisfy the wrong query slot.

Both layouts now read 'const { data: offers = null } = useClubOffers(windowId)';
useCatalog drops the listener, ICatalogOptions drops the
clubOffers / clubOffersByWindowId fields and HabboClubOffersMessageEvent
no longer needs to be imported in useCatalog. The localization-refresh
effect that re-cloned both fields is also dropped — React Query owns
the cache now, and ClubOfferData has no localized strings anyway.
This commit is contained in:
simoleo89
2026-05-11 22:23:19 +02:00
parent 2d9785e931
commit 2a5b9a4a98
6 changed files with 46 additions and 56 deletions
+1
View File
@@ -2,4 +2,5 @@ export * from './useCatalog';
export * from './useCatalogFavorites';
export * from './useCatalogPlaceMultipleItems';
export * from './useCatalogSkipPurchaseConfirmation';
export * from './useClubOffers';
export * from './useGiftConfiguration';
+1 -34
View File
@@ -1,4 +1,4 @@
import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, ClubGiftInfoEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetClubGiftInfo, GetRoomEngine, GetSessionDataManager, GetTickerTime, HabboClubOffersMessageEvent, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, NodeData, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomControllerLevel, RoomEngineObjectPlacedEvent, RoomObjectCategory, RoomObjectPlacementSource, RoomObjectType, RoomObjectVariable, RoomPreviewer, SellablePetPalettesMessageEvent, Vector3d } from '@nitrots/nitro-renderer';
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, SellablePetPalettesMessageEvent, Vector3d } from '@nitrots/nitro-renderer';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useBetween } from 'use-between';
import { BuilderFurniPlaceableStatus, CatalogNode, CatalogPage, CatalogPetPalette, 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';
@@ -739,22 +739,6 @@ const useCatalogState = () =>
});
});
useMessageEvent<HabboClubOffersMessageEvent>(HabboClubOffersMessageEvent, event =>
{
const parser = event.getParser();
setCatalogOptions(prevValue =>
{
const windowId = parser.windowId;
const clubOffersByWindowId = { ...(prevValue.clubOffersByWindowId || {}) };
clubOffersByWindowId[windowId] = parser.offers;
const clubOffers = clubOffersByWindowId[1] || prevValue.clubOffers;
return { ...prevValue, clubOffers, clubOffersByWindowId };
});
});
useMessageEvent<MarketplaceMakeOfferResult>(MarketplaceMakeOfferResult, event =>
@@ -1045,23 +1029,6 @@ const useCatalogState = () =>
return new CatalogPage(prevValue.pageId, prevValue.layoutCode, prevValue.localization, offers, prevValue.acceptSeasonCurrencyAsCredits, prevValue.mode);
});
setCatalogOptions(prevValue =>
{
if(!prevValue) return prevValue;
const clubOffersByWindowId = { ...(prevValue.clubOffersByWindowId || {}) };
Object.keys(clubOffersByWindowId).forEach(key =>
{
const offers = clubOffersByWindowId[key];
if(Array.isArray(offers)) clubOffersByWindowId[key] = [ ...offers ];
});
const clubOffers = Array.isArray(prevValue.clubOffers) ? [ ...prevValue.clubOffers ] : prevValue.clubOffers;
return { ...prevValue, clubOffers, clubOffersByWindowId };
});
};
window.addEventListener('nitro-localization-updated', refreshCatalogLocalization);
+33
View File
@@ -0,0 +1,33 @@
import { ClubOfferData, GetClubOffersMessageComposer, HabboClubOffersMessageEvent } from '@nitrots/nitro-renderer';
import { UseQueryResult } from '@tanstack/react-query';
import { useNitroQuery } from '../../api/nitro-query';
/**
* Habbo Club offer list keyed by Catalog `windowId`. windowId 1 is the
* VIP buy page; 2 / 3 are the Builders Club / Builders Club Addons
* pages. Each catalog layout asks the server for its own slice via
* GetClubOffersMessageComposer(windowId) — the server replies with a
* HabboClubOffersMessageEvent carrying parser.windowId + parser.offers.
*
* Wrapped as a TanStack query so multiple consumers reading the same
* windowId share one request, and reopening the page within the
* session-stable cache window doesn't re-fetch.
*
* The accept() predicate filters out responses tagged with a different
* windowId — the renderer multiplexes the same event for every page,
* so without the filter a slow VIP response would land in a Builders
* Club query.
*/
export const useClubOffers = (
windowId: number,
options: { enabled?: boolean } = {}
): UseQueryResult<ClubOfferData[]> =>
useNitroQuery<HabboClubOffersMessageEvent, ClubOfferData[]>({
key: [ 'nitro', 'catalog', 'clubOffers', windowId ],
request: () => new GetClubOffersMessageComposer(windowId),
parser: HabboClubOffersMessageEvent,
accept: event => (event.getParser().windowId === windowId),
select: event => (event.getParser().offers || []),
enabled: options.enabled,
staleTime: Infinity
});