vega-frontend-monorepo/apps/trading/lib/hooks/use-games.ts
2024-03-25 16:25:50 +00:00

187 lines
5.7 KiB
TypeScript

import {
type GameFieldsFragment,
type TeamEntityFragment,
GamesDocument,
type GamesQuery,
} from './__generated__/Games';
import orderBy from 'lodash/orderBy';
import { isSafeInteger, removePaginationWrapper } from '@vegaprotocol/utils';
import { useEpochInfoQuery } from './__generated__/Epoch';
import { useApolloClient, type ApolloError } from '@apollo/client';
import { TEAMS_STATS_EPOCHS } from './constants';
import { useEffect, useMemo, useState } from 'react';
const findTeam = (entities: GameFieldsFragment['entities'], teamId: string) => {
const team = entities.find(
(ent) => ent.__typename === 'TeamGameEntity' && ent.team.teamId === teamId
);
if (team?.__typename === 'TeamGameEntity') return team; // drops __typename === 'IndividualGameEntity' from team object
return undefined;
};
export type Game = GameFieldsFragment & {
/** The team entity data accessible only if scoped to particular team. */
team?: TeamEntityFragment;
};
export type TeamGame = Game & { team: NonNullable<Game['team']> };
const isTeamGame = (game: Game): game is TeamGame => game.team !== undefined;
export const areTeamGames = (games?: Game[]): games is TeamGame[] =>
Boolean(games && games.filter((g) => isTeamGame(g)).length > 0);
type GamesData = {
data?: Game[];
loading: boolean;
error?: Error | ApolloError;
};
const MAX_EPOCHS = 30;
/**
* Converts the given variables (`teamId`, `epochFrom`, `epochTo`) of
* `GamesQuery` into chunks so that the maximum difference between given
* `epochFrom` and `epochTo` is not greater than the limit of `MAX_EPOCHS`.
*
* Example: When `epochFrom == 1` and `epochTo == 59` this function should
* produce an array of variables consisting of two entries where:
* - 1st chunk: `epochFrom == 1` and `epochTo == 31`
* - 2nd chunk: `epochFrom == 32` and `epochTo == 59`
*/
const prepareVariables = (
teamId?: string,
epochFrom?: number,
epochTo?: number
) => {
let from = epochFrom;
const to = epochTo;
if (isSafeInteger(from) && from < 1) from = 1; // make sure it's not negative
let variables = [
{
teamId,
epochFrom: from,
epochTo: to,
},
];
if (isSafeInteger(from) && isSafeInteger(to)) {
// if the difference between "from" and "to" is greater than MAX_EPOCHS
// then we need to divide the variables into N chunks.
if (to - from > MAX_EPOCHS) {
const N = Math.ceil((to - from) / MAX_EPOCHS);
variables = Array(N)
.fill(null)
.map((_, i) => {
const segmentFrom = Number(from) + MAX_EPOCHS * i;
let segmentTo = Number(from) + MAX_EPOCHS * (i + 1) - 1;
if (segmentTo > to) segmentTo = to;
return {
teamId,
epochFrom: segmentFrom,
epochTo: segmentTo,
};
});
}
}
return variables;
};
export const useGames = (
teamId?: string,
epochFrom?: number,
epochTo?: number
): GamesData => {
const client = useApolloClient();
const {
data: epochData,
loading: epochLoading,
error: epochError,
} = useEpochInfoQuery({
skip: Boolean(epochFrom),
});
const [games, setGames] = useState<Game[] | undefined>(undefined);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | ApolloError | undefined>(
undefined
);
const variables = useMemo(() => {
let from = epochFrom;
let to = epochTo;
if (epochData?.epoch.id && !epochFrom) {
const currentEpoch = Number(epochData.epoch.id);
from = currentEpoch - TEAMS_STATS_EPOCHS;
to = currentEpoch;
}
if (!from) return [];
return prepareVariables(teamId, from, to);
}, [epochData?.epoch.id, epochFrom, epochTo, teamId]);
/**
* Because of the games API limitation to alway return max up to 30 epochs
* worth of data (regardless of the actual span of given variables
* `epochFrom` and `epochTo`) we need to do a trick of asking for longer
* periods in a way of chunks that are then combined into one `games`.
*
* The code below uses the direct reference to the `ApolloClient` and runs
* N queries (see `prepareVariables` function) in order to obtain the whole
* set of data.
*/
useEffect(() => {
if (loading || games || variables.length === 0) return;
if (!loading) setLoading(true);
const processChunks = async () => {
const chunks = variables.map((v) =>
client
.query<GamesQuery>({
query: GamesDocument,
variables: v,
context: { isEnlargedTimeout: true },
})
.then(({ data, loading, error }) => ({ data, loading, error }))
.catch(() => {
/* NOOP */
})
);
try {
const results = await Promise.allSettled(chunks);
const games = results.reduce((all, r) => {
if (r.status === 'fulfilled' && r.value) {
const { data, error } = r.value;
if (error) setError(error);
const allGames = removePaginationWrapper(data?.games.edges);
const allOrScoped = allGames
.map((g) => ({
...g,
team: teamId ? findTeam(g.entities, teamId) : undefined,
}))
.filter((g) => {
// passthrough if not scoped to particular team
if (!teamId) return true;
return isTeamGame(g);
});
return [...all, ...allOrScoped];
}
return all;
}, [] as Game[]);
if (games.length > 0) setGames(orderBy(games, 'epoch', 'desc'));
} finally {
setLoading(false);
}
};
processChunks();
}, [client, games, loading, teamId, variables]);
return {
data: games,
loading: loading || epochLoading,
error: error || epochError,
};
};