useNitroQuery: add accept() predicate; migrate two mod-tools chatlog views

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<ChatRecordData>(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.
This commit is contained in:
simoleo89
2026-05-11 17:00:06 +00:00
parent bb09a562f6
commit bf84a0c2a6
3 changed files with 31 additions and 36 deletions
+13 -4
View File
@@ -23,6 +23,14 @@ export interface NitroQueryConfig<TParser extends IMessageEvent, TData>
* 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 = <TParser extends IMessageEvent, TData = TParser>(
config: NitroQueryConfig<TParser, TData>
): UseQueryResult<TData> =>
{
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<TData, Error, TData> = {
queryKey: key,
queryFn: () => awaitNitroResponse<TParser, TData>({ key, request, parser, select, timeoutMs }),
queryFn: () => awaitNitroResponse<TParser, TData>({ key, request, parser, select, accept, timeoutMs }),
enabled,
staleTime,
refetchOnMount
@@ -71,11 +79,11 @@ export const useNitroQuery = <TParser extends IMessageEvent, TData = TParser>(
* can use the same plumbing imperatively.
*/
export const awaitNitroResponse = <TParser extends IMessageEvent, TData>(
config: Pick<NitroQueryConfig<TParser, TData>, 'request' | 'parser' | 'select' | 'timeoutMs'>
config: Pick<NitroQueryConfig<TParser, TData>, 'request' | 'parser' | 'select' | 'accept' | 'timeoutMs'>
): Promise<TData> =>
new Promise<TData>((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<typeof setTimeout> | null = null;
@@ -90,6 +98,7 @@ export const awaitNitroResponse = <TParser extends IMessageEvent, TData>(
listener = new (ParserCtor as any)((event: TParser) =>
{
if(settled) return;
if(accept && !accept(event)) return;
settled = true;
cleanup();
@@ -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<ModToolsChatlogViewProps> = props =>
{
const { roomId = null, onCloseClick = null } = props;
const [ roomChatlog, setRoomChatlog ] = useState<ChatRecordData>(null);
useMessageEvent<RoomChatlogEvent>(RoomChatlogEvent, event =>
{
const parser = event.getParser();
if(!parser || parser.data.roomId !== roomId) return;
setRoomChatlog(parser.data);
const { data: roomChatlog } = useNitroQuery<RoomChatlogEvent, ChatRecordData>({
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 (
@@ -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<CfhChatlogViewProps> = props =>
{
const { onCloseClick = null, issueId = null } = props;
const [ chatlogData, setChatlogData ] = useState<CfhChatlogData>(null);
useMessageEvent<CfhChatlogEvent>(CfhChatlogEvent, event =>
{
const parser = event.getParser();
if(!parser || parser.data.issueId !== issueId) return;
setChatlogData(parser.data);
const { data: chatlogData } = useNitroQuery<CfhChatlogEvent, CfhChatlogData>({
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 (
<NitroCardView className="nitro-mod-tools-chatlog" theme="primary-slim">
<NitroCardHeaderView headerText={ 'Issue Chatlog' } onCloseClick={ onCloseClick } />