From bf84a0c2a66f804b0c09717b83ab5defe5883928 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 11 May 2026 17:00:06 +0000 Subject: [PATCH] useNitroQuery: add accept() predicate; migrate two mod-tools chatlog views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Many composer/parser pairs on the Nitro wire are correlation-key based: the request carries a key (roomId, issueId, etc.) and the response shows up on the globally-shared event bus, where other components may be listening for the same parser type with a different key. The previous useNitroQuery resolved on the FIRST matching parser event regardless of key — useless for that pattern, which is why two obvious migration targets (ModToolsChatlogView, CfhChatlogView) were skipped earlier. Adapter change - New optional `accept?: (event) => boolean` on NitroQueryConfig. - In awaitNitroResponse, events for which accept returns false are IGNORED rather than resolving the promise. The listener stays registered, the timeout still applies. This lets callers do: accept: e => e.getParser()?.data.roomId === roomId Migrations - src/components/mod-tools/views/room/ModToolsChatlogView.tsx - Was: useState(null) + useMessageEvent with `if (parser.data.roomId !== roomId) return; setRoomChatlog(...)` + a mount-only useEffect dispatching the composer. - Now: a single useNitroQuery call keyed on roomId; accept filters by roomId; the query is enabled only when roomId is set. The composer is no longer re-dispatched on remount within staleTime; switching to a different room still triggers a fresh fetch because the queryKey changes. - src/components/mod-tools/views/tickets/CfhChatlogView.tsx - Same pattern, keyed on issueId. Both migrations drop ~15 lines per file (no more local state + manual listener + manual send) while gaining cache/dedup/loading/error handling from TanStack Query. Verification - yarn eslint on the four files: 1 pre-existing error (the IMessageEvent "redundant union" false positive in createNitroQuery that we already documented — local sandbox doesn't have the renderer SDK installed, so its types resolve as `any`). - yarn test: 49/49 passing. - yarn tsc on the four files: clean. --- src/api/nitro-query/createNitroQuery.ts | 17 ++++++++++--- .../views/room/ModToolsChatlogView.tsx | 25 +++++++------------ .../views/tickets/CfhChatlogView.tsx | 25 +++++++------------ 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/api/nitro-query/createNitroQuery.ts b/src/api/nitro-query/createNitroQuery.ts index 1bf73ff..44a570f 100644 --- a/src/api/nitro-query/createNitroQuery.ts +++ b/src/api/nitro-query/createNitroQuery.ts @@ -23,6 +23,14 @@ export interface NitroQueryConfig * Maps the parser event to the data the component cares about. */ select?: (event: TParser) => TData; + /** + * Optional predicate to ignore parser events that don't match this + * query (typically used as a correlation-key filter on a globally + * shared event stream — e.g. `e => e.getParser()?.roomId === roomId`). + * When the predicate returns false, the listener stays registered + * and keeps waiting; the timeout still applies. + */ + accept?: (event: TParser) => boolean; /** * Max time to wait for the response before rejecting (default 15s). */ @@ -52,11 +60,11 @@ export const useNitroQuery = ( config: NitroQueryConfig ): UseQueryResult => { - const { key, request, parser, select, timeoutMs = 15_000, enabled, staleTime, refetchOnMount } = config; + const { key, request, parser, select, accept, timeoutMs = 15_000, enabled, staleTime, refetchOnMount } = config; const options: UseQueryOptions = { queryKey: key, - queryFn: () => awaitNitroResponse({ key, request, parser, select, timeoutMs }), + queryFn: () => awaitNitroResponse({ key, request, parser, select, accept, timeoutMs }), enabled, staleTime, refetchOnMount @@ -71,11 +79,11 @@ export const useNitroQuery = ( * can use the same plumbing imperatively. */ export const awaitNitroResponse = ( - config: Pick, 'request' | 'parser' | 'select' | 'timeoutMs'> + config: Pick, 'request' | 'parser' | 'select' | 'accept' | 'timeoutMs'> ): Promise => new Promise((resolve, reject) => { - const { request, parser: ParserCtor, select, timeoutMs = 15_000 } = config; + const { request, parser: ParserCtor, select, accept, timeoutMs = 15_000 } = config; let settled = false; let timeoutHandle: ReturnType | null = null; @@ -90,6 +98,7 @@ export const awaitNitroResponse = ( listener = new (ParserCtor as any)((event: TParser) => { if(settled) return; + if(accept && !accept(event)) return; settled = true; cleanup(); diff --git a/src/components/mod-tools/views/room/ModToolsChatlogView.tsx b/src/components/mod-tools/views/room/ModToolsChatlogView.tsx index b3a4a39..65061be 100644 --- a/src/components/mod-tools/views/room/ModToolsChatlogView.tsx +++ b/src/components/mod-tools/views/room/ModToolsChatlogView.tsx @@ -1,8 +1,7 @@ import { ChatRecordData, GetRoomChatlogMessageComposer, RoomChatlogEvent } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; -import { SendMessageComposer } from '../../../../api'; +import { FC } from 'react'; +import { useNitroQuery } from '../../../../api/nitro-query'; import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; -import { useMessageEvent } from '../../../../hooks'; import { ChatlogView } from '../chatlog/ChatlogView'; interface ModToolsChatlogViewProps @@ -14,22 +13,16 @@ interface ModToolsChatlogViewProps export const ModToolsChatlogView: FC = props => { const { roomId = null, onCloseClick = null } = props; - const [ roomChatlog, setRoomChatlog ] = useState(null); - useMessageEvent(RoomChatlogEvent, event => - { - const parser = event.getParser(); - - if(!parser || parser.data.roomId !== roomId) return; - - setRoomChatlog(parser.data); + const { data: roomChatlog } = useNitroQuery({ + key: [ 'nitro', 'mod-tools', 'room-chatlog', roomId ], + request: () => new GetRoomChatlogMessageComposer(roomId), + parser: RoomChatlogEvent, + accept: e => e.getParser()?.data.roomId === roomId, + select: e => e.getParser().data, + enabled: roomId !== null }); - useEffect(() => - { - SendMessageComposer(new GetRoomChatlogMessageComposer(roomId)); - }, [ roomId ]); - if(!roomChatlog) return null; return ( diff --git a/src/components/mod-tools/views/tickets/CfhChatlogView.tsx b/src/components/mod-tools/views/tickets/CfhChatlogView.tsx index 9923fa9..33cc52d 100644 --- a/src/components/mod-tools/views/tickets/CfhChatlogView.tsx +++ b/src/components/mod-tools/views/tickets/CfhChatlogView.tsx @@ -1,8 +1,7 @@ import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; -import { SendMessageComposer } from '../../../../api'; +import { FC } from 'react'; +import { useNitroQuery } from '../../../../api/nitro-query'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; -import { useMessageEvent } from '../../../../hooks'; import { ChatlogView } from '../chatlog/ChatlogView'; interface CfhChatlogViewProps @@ -14,22 +13,16 @@ interface CfhChatlogViewProps export const CfhChatlogView: FC = props => { const { onCloseClick = null, issueId = null } = props; - const [ chatlogData, setChatlogData ] = useState(null); - useMessageEvent(CfhChatlogEvent, event => - { - const parser = event.getParser(); - - if(!parser || parser.data.issueId !== issueId) return; - - setChatlogData(parser.data); + const { data: chatlogData } = useNitroQuery({ + key: [ 'nitro', 'mod-tools', 'cfh-chatlog', issueId ], + request: () => new GetCfhChatlogMessageComposer(issueId), + parser: CfhChatlogEvent, + accept: e => e.getParser()?.data.issueId === issueId, + select: e => e.getParser().data, + enabled: issueId !== null }); - useEffect(() => - { - SendMessageComposer(new GetCfhChatlogMessageComposer(issueId)); - }, [ issueId ]); - return (