T
TanStack10mo ago
rising-crimson

chat long poll

Hi all, I'm currently building out a chat application. The back-end doesn't yet support web socket connections, so we're relying on long-polling to get chat messages. I'm trying to build a refetch decay, and just wanted to check-in on best practices. FWIW, it is working.
export type UseFetchChatDetailsQueryOptions<T> = {
// The chat ID to fetch
chatId?: string;
// The max amount of time between polling requests. Default: 9 minutes (540 seconds)
maxPollingTimeSeconds?: number;
// The default interval between polling. Default: 1000
defaultIntervalMs?: number;
// The max interval between polling. Default: 3000
maxIntervalMs?: number;
// The decay factor for the polling interval. Default: 1.1
decayFactor?: number;
// A selector function to transform the data
select?: (data: Awaited<ReturnType<typeof fetchChatDetails>>) => T;
};

export const useFetchChatDetailsQuery = <T>({
chatId,
defaultIntervalMs = 1000,
maxPollingTimeSeconds = 9 * 60,
decayFactor = 1.1,
maxIntervalMs = 3000,
select,
}: UseFetchChatDetailsQueryOptions<T>) => {
const refetchIntervalRef = useRef(defaultIntervalMs);

return useQuery({
...fetchChatDetailsQueryOptions(chatId),
select,
refetchInterval: ({ state: { data, fetchStatus } }) => {
// Only re-compute the interval when the query is idle.
if (fetchStatus !== "idle") {
return false;
}

if (!data) {
console.debug("No data found, returning false");
return defaultIntervalMs;
}

const updatedAt = DateTime.fromMillis(data.lastUpdateAttempt);
const timeoutDifference = DateTime.now().diff(updatedAt).as("seconds");
const isTimedOut = timeoutDifference > maxPollingTimeSeconds;
const isTerminal = data.status === "FAILURE" || data.status === "SUCCESS";

if (isTimedOut || isTerminal) {
// Reset the interval so subsequent refetches start over.
refetchIntervalRef.current = defaultIntervalMs;
console.debug("Query is terminal or timed out, returning false");
return false;
}

const interval = Math.min(
Math.floor(refetchIntervalRef.current * decayFactor),
maxIntervalMs
);

refetchIntervalRef.current = interval;
console.debug("Returning interval:", interval);

return interval;
},
});
};
export type UseFetchChatDetailsQueryOptions<T> = {
// The chat ID to fetch
chatId?: string;
// The max amount of time between polling requests. Default: 9 minutes (540 seconds)
maxPollingTimeSeconds?: number;
// The default interval between polling. Default: 1000
defaultIntervalMs?: number;
// The max interval between polling. Default: 3000
maxIntervalMs?: number;
// The decay factor for the polling interval. Default: 1.1
decayFactor?: number;
// A selector function to transform the data
select?: (data: Awaited<ReturnType<typeof fetchChatDetails>>) => T;
};

export const useFetchChatDetailsQuery = <T>({
chatId,
defaultIntervalMs = 1000,
maxPollingTimeSeconds = 9 * 60,
decayFactor = 1.1,
maxIntervalMs = 3000,
select,
}: UseFetchChatDetailsQueryOptions<T>) => {
const refetchIntervalRef = useRef(defaultIntervalMs);

return useQuery({
...fetchChatDetailsQueryOptions(chatId),
select,
refetchInterval: ({ state: { data, fetchStatus } }) => {
// Only re-compute the interval when the query is idle.
if (fetchStatus !== "idle") {
return false;
}

if (!data) {
console.debug("No data found, returning false");
return defaultIntervalMs;
}

const updatedAt = DateTime.fromMillis(data.lastUpdateAttempt);
const timeoutDifference = DateTime.now().diff(updatedAt).as("seconds");
const isTimedOut = timeoutDifference > maxPollingTimeSeconds;
const isTerminal = data.status === "FAILURE" || data.status === "SUCCESS";

if (isTimedOut || isTerminal) {
// Reset the interval so subsequent refetches start over.
refetchIntervalRef.current = defaultIntervalMs;
console.debug("Query is terminal or timed out, returning false");
return false;
}

const interval = Math.min(
Math.floor(refetchIntervalRef.current * decayFactor),
maxIntervalMs
);

refetchIntervalRef.current = interval;
console.debug("Returning interval:", interval);

return interval;
},
});
};
I would have expected to get the previous refetch interval from some query state, but didn't find anything in the API docs or code to suggest that was possible, so I'm using the ref. It just feels a bit strange, so I wanted to make sure there was nothing obviously wrong with this approach. Thanks for looking!
1 Reply
stormy-gold
stormy-gold10mo ago
good idea I am very good at this part. so if you can work with me, you will get good result.s thanks.

Did you find this page helpful?