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'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { SelectGames } from '@/server/db/types'
|
||||
import { api } from '@/trpc/react'
|
||||
import {
|
||||
type SortingState,
|
||||
createColumnHelper,
|
||||
@@ -42,11 +43,6 @@ const numberFormatter = new Intl.NumberFormat('en-US', {
|
||||
})
|
||||
|
||||
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
|
||||
const useColumns = (openTranscriptFn?: (gameNumber: number) => void) => {
|
||||
const format = useFormatter()
|
||||
@@ -197,22 +193,25 @@ const useColumns = (openTranscriptFn?: (gameNumber: number) => void) => {
|
||||
export function GamesTable({ games }: { games: SelectGames[] }) {
|
||||
const [sorting, setSorting] = useState<SortingState>([])
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [transcriptContent, setTranscriptContent] = useState<string>('')
|
||||
const [transcriptGameNumber, setTranscriptGameNumber] = useState<
|
||||
number | 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
|
||||
const openTranscript = (gameNumber: number): void => {
|
||||
setTranscriptGameNumber(gameNumber)
|
||||
getTranscript(gameNumber)
|
||||
.then((html: string) => {
|
||||
setTranscriptContent(html)
|
||||
setIsDialogOpen(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to load transcript:', err)
|
||||
})
|
||||
setIsDialogOpen(true)
|
||||
}
|
||||
|
||||
// Pass the openTranscript function to useColumns
|
||||
@@ -308,12 +307,25 @@ export function GamesTable({ games }: { games: SelectGames[] }) {
|
||||
</DialogHeader>
|
||||
{/* Use iframe to isolate the transcript content and prevent style leakage */}
|
||||
<div className='!h-[60vh] mt-4 w-full'>
|
||||
<iframe
|
||||
srcDoc={transcriptContent}
|
||||
title={`Game Transcript #${transcriptGameNumber || ''}`}
|
||||
className='h-full w-full border-0'
|
||||
sandbox='allow-same-origin'
|
||||
/>
|
||||
{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
|
||||
srcDoc={transcriptContent}
|
||||
title={`Game Transcript #${transcriptGameNumber || ''}`}
|
||||
className='h-full w-full border-0'
|
||||
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>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -5,8 +5,18 @@ import { and, desc, eq, gt, lt, sql } from 'drizzle-orm'
|
||||
import ky from 'ky'
|
||||
import { chunk } from 'remeda'
|
||||
import { z } from 'zod'
|
||||
import { neatqueue_service } from '@/server/services/neatqueue.service'
|
||||
|
||||
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
|
||||
.input(
|
||||
z
|
||||
|
||||
@@ -189,3 +189,10 @@ export const leaderboardSnapshots = pgTable('leaderboard_snapshots', {
|
||||
})
|
||||
|
||||
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 { 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'
|
||||
|
||||
@@ -9,6 +13,9 @@ const instance = ky.create({
|
||||
|
||||
const BMM_SERVER_ID = '1226193436521267223'
|
||||
|
||||
// Redis key for transcript cache
|
||||
export const TRANSCRIPT_CACHE_KEY = (gameNumber: number) => `transcript:${gameNumber}`
|
||||
|
||||
export const neatqueue_service = {
|
||||
get_leaderboard: async (channel_id: string) => {
|
||||
const res = await instance
|
||||
@@ -44,6 +51,52 @@ export const neatqueue_service = {
|
||||
.json()
|
||||
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 = {
|
||||
mmr: number
|
||||
|
||||
Reference in New Issue
Block a user