mirror of
https://github.com/ershisan99/www.git
synced 2025-12-17 05:19:23 +00:00
store transcripts in redis/pg to alleviate the load on nq
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
|||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import type { SelectGames } from '@/server/db/types'
|
import type { SelectGames } from '@/server/db/types'
|
||||||
|
import { api } from '@/trpc/react'
|
||||||
import {
|
import {
|
||||||
type SortingState,
|
type SortingState,
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
@@ -42,11 +43,6 @@ const numberFormatter = new Intl.NumberFormat('en-US', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<SelectGames>()
|
const columnHelper = createColumnHelper<SelectGames>()
|
||||||
function getTranscript(gameNumber: number) {
|
|
||||||
return fetch(
|
|
||||||
`https://api.neatqueue.com/api/transcript/1226193436521267223/${gameNumber}`
|
|
||||||
).then((res) => res.json())
|
|
||||||
}
|
|
||||||
// This function is now moved inside the GamesTable component
|
// This function is now moved inside the GamesTable component
|
||||||
const useColumns = (openTranscriptFn?: (gameNumber: number) => void) => {
|
const useColumns = (openTranscriptFn?: (gameNumber: number) => void) => {
|
||||||
const format = useFormatter()
|
const format = useFormatter()
|
||||||
@@ -197,22 +193,25 @@ const useColumns = (openTranscriptFn?: (gameNumber: number) => void) => {
|
|||||||
export function GamesTable({ games }: { games: SelectGames[] }) {
|
export function GamesTable({ games }: { games: SelectGames[] }) {
|
||||||
const [sorting, setSorting] = useState<SortingState>([])
|
const [sorting, setSorting] = useState<SortingState>([])
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
const [transcriptContent, setTranscriptContent] = useState<string>('')
|
|
||||||
const [transcriptGameNumber, setTranscriptGameNumber] = useState<
|
const [transcriptGameNumber, setTranscriptGameNumber] = useState<
|
||||||
number | null
|
number | null
|
||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
|
// Use the tRPC useQuery hook to fetch the transcript
|
||||||
|
const { data: transcriptContent, isLoading } = api.history.getTranscript.useQuery(
|
||||||
|
{ gameNumber: transcriptGameNumber ?? 0 },
|
||||||
|
{
|
||||||
|
// Only fetch when we have a game number and the dialog is open
|
||||||
|
enabled: transcriptGameNumber !== null && isDialogOpen,
|
||||||
|
// Don't refetch on window focus
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// New openTranscript function that sets state instead of opening a new window
|
// New openTranscript function that sets state instead of opening a new window
|
||||||
const openTranscript = (gameNumber: number): void => {
|
const openTranscript = (gameNumber: number): void => {
|
||||||
setTranscriptGameNumber(gameNumber)
|
setTranscriptGameNumber(gameNumber)
|
||||||
getTranscript(gameNumber)
|
|
||||||
.then((html: string) => {
|
|
||||||
setTranscriptContent(html)
|
|
||||||
setIsDialogOpen(true)
|
setIsDialogOpen(true)
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error('Failed to load transcript:', err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the openTranscript function to useColumns
|
// Pass the openTranscript function to useColumns
|
||||||
@@ -308,12 +307,25 @@ export function GamesTable({ games }: { games: SelectGames[] }) {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{/* Use iframe to isolate the transcript content and prevent style leakage */}
|
{/* Use iframe to isolate the transcript content and prevent style leakage */}
|
||||||
<div className='!h-[60vh] mt-4 w-full'>
|
<div className='!h-[60vh] mt-4 w-full'>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className='flex h-full w-full items-center justify-center'>
|
||||||
|
<div className='text-center'>
|
||||||
|
<div className='mb-2 h-6 w-6 animate-spin rounded-full border-b-2 border-t-2 border-gray-900 dark:border-gray-100'></div>
|
||||||
|
<p>Loading transcript...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : transcriptContent ? (
|
||||||
<iframe
|
<iframe
|
||||||
srcDoc={transcriptContent}
|
srcDoc={transcriptContent}
|
||||||
title={`Game Transcript #${transcriptGameNumber || ''}`}
|
title={`Game Transcript #${transcriptGameNumber || ''}`}
|
||||||
className='h-full w-full border-0'
|
className='h-full w-full border-0'
|
||||||
sandbox='allow-same-origin'
|
sandbox='allow-same-origin'
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className='flex h-full w-full items-center justify-center'>
|
||||||
|
<p>Failed to load transcript. Please try again.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -5,8 +5,18 @@ import { and, desc, eq, gt, lt, sql } from 'drizzle-orm'
|
|||||||
import ky from 'ky'
|
import ky from 'ky'
|
||||||
import { chunk } from 'remeda'
|
import { chunk } from 'remeda'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { neatqueue_service } from '@/server/services/neatqueue.service'
|
||||||
|
|
||||||
export const history_router = createTRPCRouter({
|
export const history_router = createTRPCRouter({
|
||||||
|
getTranscript: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
gameNumber: z.number(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await neatqueue_service.get_transcript(input.gameNumber);
|
||||||
|
}),
|
||||||
games_per_hour: publicProcedure
|
games_per_hour: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z
|
z
|
||||||
|
|||||||
@@ -189,3 +189,10 @@ export const leaderboardSnapshots = pgTable('leaderboard_snapshots', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const leaderboardSnapshotsRelations = relations(leaderboardSnapshots, ({}) => ({}))
|
export const leaderboardSnapshotsRelations = relations(leaderboardSnapshots, ({}) => ({}))
|
||||||
|
|
||||||
|
export const transcripts = pgTable('transcripts', {
|
||||||
|
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
|
||||||
|
gameNumber: integer('game_number').notNull().unique(),
|
||||||
|
content: text('content').notNull(),
|
||||||
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import ky from 'ky'
|
import ky from 'ky'
|
||||||
|
import { db } from '../db'
|
||||||
|
import { redis } from '../redis'
|
||||||
|
import { transcripts } from '../db/schema'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
|
||||||
const NEATQUEUE_URL = 'https://api.neatqueue.com/api'
|
const NEATQUEUE_URL = 'https://api.neatqueue.com/api'
|
||||||
|
|
||||||
@@ -9,6 +13,9 @@ const instance = ky.create({
|
|||||||
|
|
||||||
const BMM_SERVER_ID = '1226193436521267223'
|
const BMM_SERVER_ID = '1226193436521267223'
|
||||||
|
|
||||||
|
// Redis key for transcript cache
|
||||||
|
export const TRANSCRIPT_CACHE_KEY = (gameNumber: number) => `transcript:${gameNumber}`
|
||||||
|
|
||||||
export const neatqueue_service = {
|
export const neatqueue_service = {
|
||||||
get_leaderboard: async (channel_id: string) => {
|
get_leaderboard: async (channel_id: string) => {
|
||||||
const res = await instance
|
const res = await instance
|
||||||
@@ -44,6 +51,52 @@ export const neatqueue_service = {
|
|||||||
.json()
|
.json()
|
||||||
return response
|
return response
|
||||||
},
|
},
|
||||||
|
get_transcript: async (gameNumber: number, server_id: string = BMM_SERVER_ID) => {
|
||||||
|
// Try to get from Redis cache first (fastest)
|
||||||
|
const cacheKey = TRANSCRIPT_CACHE_KEY(gameNumber)
|
||||||
|
const cachedTranscript = await redis.get(cacheKey)
|
||||||
|
|
||||||
|
if (cachedTranscript) {
|
||||||
|
console.log(`Transcript #${gameNumber} found in Redis cache`)
|
||||||
|
return cachedTranscript
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in Redis, try to get from database
|
||||||
|
const dbTranscript = await db.query.transcripts.findFirst({
|
||||||
|
where: eq(transcripts.gameNumber, gameNumber)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (dbTranscript) {
|
||||||
|
console.log(`Transcript #${gameNumber} found in database`)
|
||||||
|
// Store in Redis for future quick access
|
||||||
|
await redis.set(cacheKey, dbTranscript.content)
|
||||||
|
return dbTranscript.content
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in database, fetch from neatqueue
|
||||||
|
console.log(`Fetching transcript #${gameNumber} from neatqueue`)
|
||||||
|
try {
|
||||||
|
const response = await instance
|
||||||
|
.get(`transcript/${server_id}/${gameNumber}`)
|
||||||
|
.json<string>()
|
||||||
|
|
||||||
|
// Store in both database and Redis
|
||||||
|
await db.insert(transcripts).values({
|
||||||
|
gameNumber,
|
||||||
|
content: response
|
||||||
|
}).onConflictDoUpdate({
|
||||||
|
target: transcripts.gameNumber,
|
||||||
|
set: { content: response }
|
||||||
|
})
|
||||||
|
|
||||||
|
await redis.set(cacheKey, response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching transcript #${gameNumber}:`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
export type Data = {
|
export type Data = {
|
||||||
mmr: number
|
mmr: number
|
||||||
|
|||||||
Reference in New Issue
Block a user