From e2ec33998807cc08931f98150107813c519539e4 Mon Sep 17 00:00:00 2001 From: Arvind Date: Sun, 17 May 2026 13:32:36 +0530 Subject: [PATCH 1/2] Add in-memory cache for current board --- src/services/game/puzzle.ts | 183 ++++++++++++++++++++---------------- 1 file changed, 100 insertions(+), 83 deletions(-) diff --git a/src/services/game/puzzle.ts b/src/services/game/puzzle.ts index 906534c..ce7e172 100644 --- a/src/services/game/puzzle.ts +++ b/src/services/game/puzzle.ts @@ -1,20 +1,46 @@ import { DailyPuzzle } from "@prisma/client"; import { prisma } from "@/lib"; + +const cachedBoardsByKey = new Map(); +const cachedBoardsById = new Map(); +const cacheExpiryTimers = new Map>(); + +const normalizeDate = (date: string): string => new Date(date).toISOString().split('T')[0]; +const getCacheKey = (date: string, boardSize: number): string => `${date}:${boardSize}`; + +const getExpiryMillis = (date: string): number => { + const normalizedDate = normalizeDate(date); + const puzzleDate = new Date(`${normalizedDate}T00:00:00.000Z`); + const nextDay = new Date(puzzleDate); + nextDay.setUTCDate(nextDay.getUTCDate() + 1); + return Math.max(0, nextDay.getTime() - Date.now()); +}; + +const scheduleCacheEviction = (cacheKey: string, puzzleId: number, ttl: number): void => { + const existingTimer = cacheExpiryTimers.get(cacheKey); + if (existingTimer) { + clearTimeout(existingTimer); + } + + const timer = setTimeout(() => { + cacheExpiryTimers.delete(cacheKey); + cachedBoardsByKey.delete(cacheKey); + cachedBoardsById.delete(puzzleId); + }, ttl); + + cacheExpiryTimers.set(cacheKey, timer); +}; + export function fetchBoard(boardSize: number): { x: number, y: number }[] { const randomCoordinates: { x: number, y: number }[] = []; - - // Start with a random position const startX = Math.floor(Math.random() * boardSize); const startY = Math.floor(Math.random() * boardSize); - randomCoordinates.push({ x: startX, y: startY }); - const targetCount = boardSize; + randomCoordinates.push({ x: startX, y: startY }); - while (randomCoordinates.length < targetCount) { + while (randomCoordinates.length < boardSize) { const randIndex = Math.floor(Math.random() * randomCoordinates.length); const lastCoord = randomCoordinates[randIndex]; - - // Possible directions: up, down, left, right const directions = [ { dx: -1, dy: 0 }, { dx: 1, dy: 0 }, @@ -22,15 +48,12 @@ export function fetchBoard(boardSize: number): { x: number, y: number }[] { { dx: 0, dy: 1 } ]; - // Shuffle directions for randomness directions.sort(() => Math.random() - 0.5); - // Try each direction until we find a valid new position for (const { dx, dy } of directions) { const newX = lastCoord.x + dx; const newY = lastCoord.y + dy; - // Check if the new position is valid and not already used if ( newX >= 0 && newX < boardSize && newY >= 0 && newY < boardSize && @@ -42,11 +65,6 @@ export function fetchBoard(boardSize: number): { x: number, y: number }[] { } } - // const board = Array.from({ length: boardSize }, (_, row) => - // Array.from({ length: boardSize }, (_, col) => - // randomCoordinates.some(({ x, y }) => x === row && y === col) ? 'X' : '' - // ) - // ); return randomCoordinates; } @@ -55,56 +73,81 @@ interface getCurrentBoardParams { boardSize?: number; date?: string; } -export async function getCurrentBoard({ puzzleId, boardSize, date }: getCurrentBoardParams): Promise { - try { - console.log("puzzleId in getCurrentBoard", puzzleId, boardSize, date); - if (puzzleId) { - const dailyPuzzle = await prisma.dailyPuzzle.findUnique({ - where: { - id: puzzleId - } - }); - if (!dailyPuzzle) { - throw new Error("Invalid puzzleId"); + +const cacheDailyPuzzle = (dailyPuzzle: DailyPuzzle): void => { + const normalizedDate = normalizeDate(dailyPuzzle.date.toISOString().split('T')[0]); + const cacheKey = getCacheKey(normalizedDate, dailyPuzzle.boardSize); + + cachedBoardsByKey.set(cacheKey, dailyPuzzle); + cachedBoardsById.set(dailyPuzzle.id, dailyPuzzle); + scheduleCacheEviction(cacheKey, dailyPuzzle.id, getExpiryMillis(normalizedDate)); +}; + +const getCachedCurrentBoard = async (date: string, boardSize: number): Promise => { + const normalizedDate = normalizeDate(date); + const cacheKey = getCacheKey(normalizedDate, boardSize); + const cached = cachedBoardsByKey.get(cacheKey); + if (cached) { + return cached; + } + + const puzzleDate = new Date(`${normalizedDate}T00:00:00.000Z`); + let dailyPuzzle = await prisma.dailyPuzzle.findUnique({ + where: { + date_boardSize: { + date: puzzleDate, + boardSize } - return dailyPuzzle; } - if (!date || !boardSize) { - throw new Error("Invalid date or boardSize"); - } - const puzzleDate = new Date(date); - let dailyPuzzle = await prisma.dailyPuzzle.findUnique({ - where: { - date_boardSize: { - date: puzzleDate, - boardSize - } - } - }); - console.log("dailyPuzzle in getCurrentBoard before fetchBoard", dailyPuzzle); - if (!dailyPuzzle) { - const board = fetchBoard(boardSize); - const puzzleData = { + }); + + if (!dailyPuzzle) { + const board = fetchBoard(boardSize); + dailyPuzzle = await prisma.dailyPuzzle.create({ + data: { date: puzzleDate, board, - boardSize: boardSize + boardSize } - console.log("current board in getCurrentBoard before create", puzzleData); - dailyPuzzle = await prisma.dailyPuzzle.create({ - data: puzzleData - }).catch((error) => { - console.error("Error prisma create daily puzzle", error); - throw error; - }); - console.log("dailyPuzzle in getCurrentBoard after create", dailyPuzzle); + }); + } + + cacheDailyPuzzle(dailyPuzzle); + return dailyPuzzle; +}; + +const getCachedCurrentBoardById = async (puzzleId: number): Promise => { + const cached = cachedBoardsById.get(puzzleId); + if (cached) { + return cached; + } + + const dailyPuzzle = await prisma.dailyPuzzle.findUnique({ + where: { + id: puzzleId } - console.log("dailyPuzzle in getCurrentBoard", dailyPuzzle); - return dailyPuzzle - } catch (error) { - console.error("Error fetching current board:", error); - throw error; + }); + + if (!dailyPuzzle) { + throw new Error("Invalid puzzleId"); + } + + cacheDailyPuzzle(dailyPuzzle); + return dailyPuzzle; +}; + +export async function getCurrentBoard({ puzzleId, boardSize, date }: getCurrentBoardParams): Promise { + if (puzzleId) { + return getCachedCurrentBoardById(puzzleId); } + + if (!date || !boardSize) { + throw new Error("Invalid date or boardSize"); + } + + return getCachedCurrentBoard(date, boardSize); } + export function getAdjacentCount(board: { x: number, y: number }[], boardSize: number, x: number, y: number): number { let adjacentCount = 0; const directions = [ @@ -130,31 +173,5 @@ export function getAdjacentCount(board: { x: number, y: number }[], boardSize: n } export function checkGuess(board: { x: number, y: number }[], guess: string[][]): boolean { - // console.log("board in checkGuess", board); - // console.log("guess in checkGuess", guess); return board.every(({ x, y }) => guess[x][y] === 'X'); } - -// export async function updateProgress(userId: string, date: string, attempts: number): Promise { -// try { -// const puzzleDate = new Date(date); -// let progress = await prisma.userProgress.findUnique({ -// where: { -// userId_puzzleDate: { userId, puzzleDate } -// } -// }); -// if (!progress) { -// progress = await prisma.userProgress.create({ -// data: { userId, puzzleDate, completed: true, moves: attempts } -// }); -// } else { -// progress = await prisma.userProgress.update({ -// where: { userId_puzzleDate: { userId, puzzleDate } }, -// data: { completed: true, moves: attempts } -// }); -// } -// } catch (error) { -// console.error("Error updating progress:", error); -// throw error; -// } -// } \ No newline at end of file From 8c6cb2e92d62d2b6552f39f69c4ea71b981f7438 Mon Sep 17 00:00:00 2001 From: Arvind Date: Tue, 19 May 2026 16:39:24 +0530 Subject: [PATCH 2/2] refactor: unifying board cache using node-cache with (date+boardSize) key --- app/api/puzzle/check/route.ts | 4 +- app/api/puzzle/hint/route.ts | 5 +- package.json | 2 + pnpm-lock.yaml | 40 +++++++-- src/api/daily-api.ts | 8 +- src/contexts/puzzle/game-settings-context.tsx | 4 +- src/lib/validation/guess-check-validation.ts | 7 +- src/lib/validation/hint-validation.ts | 10 ++- src/services/game/puzzle.ts | 90 +++---------------- 9 files changed, 71 insertions(+), 99 deletions(-) diff --git a/app/api/puzzle/check/route.ts b/app/api/puzzle/check/route.ts index e35fa0b..31d3d70 100644 --- a/app/api/puzzle/check/route.ts +++ b/app/api/puzzle/check/route.ts @@ -12,9 +12,9 @@ export async function POST(request: NextRequest): Promise { return NextResponse.json({ errors }, { status: 400 }); } - const { puzzleId, guess } = data; + const { date, boardSize, guess } = data; - const currentBoard = await getCurrentBoard({ puzzleId }); + const currentBoard = await getCurrentBoard({ date, boardSize }); const isCorrect = checkGuess(currentBoard.board as { x: number, y: number }[], guess); const updatedUserProgress = await updateUserProgress({ userId: data.userId!, boardSize: currentBoard.boardSize, puzzleId: currentBoard.id, status: isCorrect ? 'CORRECT' : 'WRONG' }); const statistics = await getStatistics(userId!); diff --git a/app/api/puzzle/hint/route.ts b/app/api/puzzle/hint/route.ts index 4cb05d0..7829b68 100644 --- a/app/api/puzzle/hint/route.ts +++ b/app/api/puzzle/hint/route.ts @@ -12,10 +12,9 @@ export async function GET(request: Request): Promise { if (!isValid || !data) { return NextResponse.json({ errors }, { status: 400 }); } - const { puzzleId, x, y } = data; + const { date, boardSize, x, y } = data; - const currentBoard = await getCurrentBoard({ puzzleId }); - console.log("Current board in hint",currentBoard) + const currentBoard = await getCurrentBoard({ date, boardSize }); const board = currentBoard.board as { x: number, y: number }[]; const adjacentCount = getAdjacentCount(board, currentBoard.boardSize, x, y); diff --git a/package.json b/package.json index d549eba..c4c872b 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "canvas-confetti": "^1.9.3", "date-fns": "^4.1.0", "next": "15.0.7", + "node-cache": "^5.1.2", "react": "19.0.0-rc-02c0e824-20241028", "react-dom": "19.0.0-rc-02c0e824-20241028", "react-icons": "^5.3.0", @@ -25,6 +26,7 @@ "devDependencies": { "@types/canvas-confetti": "^1.6.4", "@types/node": "^20", + "@types/node-cache": "^4.2.5", "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c4bbc1..7a90821 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: next: specifier: 15.0.7 version: 15.0.7(react-dom@19.0.0-rc-02c0e824-20241028(react@19.0.0-rc-02c0e824-20241028))(react@19.0.0-rc-02c0e824-20241028) + node-cache: + specifier: ^5.1.2 + version: 5.1.2 react: specifier: 19.0.0-rc-02c0e824-20241028 version: 19.0.0-rc-02c0e824-20241028 @@ -48,6 +51,9 @@ importers: '@types/node': specifier: ^20 version: 20.17.6 + '@types/node-cache': + specifier: ^4.2.5 + version: 4.2.5 '@types/react': specifier: ^18 version: 18.3.12 @@ -372,6 +378,10 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/node-cache@4.2.5': + resolution: {integrity: sha512-faK2Owokboz53g8ooq2dw3iDJ6/HMTCIa2RvMte5WMTiABy+wA558K+iuyRtlR67Un5q9gEKysSDtqZYbSa0Pg==} + deprecated: This is a stub types definition. node-cache provides its own type definitions, so you do not need this installed. + '@types/node@20.17.6': resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==} @@ -593,6 +603,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1289,6 +1303,10 @@ packages: sass: optional: true + node-cache@5.1.2: + resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} + engines: {node: '>= 8.0.0'} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -2018,6 +2036,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/node-cache@4.2.5': + dependencies: + node-cache: 5.1.2 + '@types/node@20.17.6': dependencies: undici-types: 6.19.8 @@ -2293,6 +2315,8 @@ snapshots: client-only@0.0.1: {} + clone@2.1.2: {} + clsx@2.1.1: {} color-convert@2.0.1: @@ -2506,7 +2530,7 @@ snapshots: '@typescript-eslint/parser': 8.13.0(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.2(eslint@8.57.1) @@ -2526,13 +2550,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -2545,14 +2569,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.13.0(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -2567,7 +2591,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.13.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -3124,6 +3148,10 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-cache@5.1.2: + dependencies: + clone: 2.1.2 + normalize-path@3.0.0: {} object-assign@4.1.1: {} diff --git a/src/api/daily-api.ts b/src/api/daily-api.ts index 30f6019..4788d8b 100644 --- a/src/api/daily-api.ts +++ b/src/api/daily-api.ts @@ -17,9 +17,9 @@ export const getGameStatus = async (date: string, boardSize: number): Promise => { +export const getHint = async (date: string, boardSize: number, x: number, y: number): Promise => { try { - const response = await axiosSecure.get(`${API_HINT}?puzzleId=${puzzleId}&x=${x}&y=${y}`); + const response = await axiosSecure.get(`${API_HINT}?date=${date}&boardSize=${boardSize}&x=${x}&y=${y}`); return response.data; } catch (error) { if (error instanceof AxiosError && error.code === "ERR_BAD_RESPONSE") { @@ -30,9 +30,9 @@ export const getHint = async (puzzleId: number, x: number, y: number): Promise => { +export const checkGuess = async (date: string, boardSize: number, guess: string[][], attempts: number): Promise => { try { - const response = await axiosSecure.post(`${API_CHECK_GUESS}`, { puzzleId, guess, attempts }); + const response = await axiosSecure.post(`${API_CHECK_GUESS}`, { date, boardSize, guess, attempts }); return response.data; } catch (error) { if (error instanceof AxiosError && error.code === "ERR_BAD_RESPONSE") { diff --git a/src/contexts/puzzle/game-settings-context.tsx b/src/contexts/puzzle/game-settings-context.tsx index 12b8fb9..b4d35d9 100644 --- a/src/contexts/puzzle/game-settings-context.tsx +++ b/src/contexts/puzzle/game-settings-context.tsx @@ -86,7 +86,7 @@ export function GameSettingsProvider({ children }: { children: React.ReactNode } try { setLoadingCoordinates({ x, y }); updateSettings({ hints: settings.hints + 1 }); - const data = await getHint(settings.puzzleId, x, y); + const data = await getHint(settings.date, settings.boardSize, x, y); const newBoard = [...settings.board]; newBoard[x][y] = data.adjacentCount.toString(); updateSettings({ board: newBoard }); @@ -104,7 +104,7 @@ export function GameSettingsProvider({ children }: { children: React.ReactNode } try { updateSettings({ gameStatus: "guess-loading" }); const [response,] = await Promise.all([ - checkGuess(settings.puzzleId, settings.guess, settings.hints), + checkGuess(settings.date, settings.boardSize, settings.guess, settings.hints), new Promise(resolve => setTimeout(resolve, 2000)) ]); diff --git a/src/lib/validation/guess-check-validation.ts b/src/lib/validation/guess-check-validation.ts index 6ef4efc..e838757 100644 --- a/src/lib/validation/guess-check-validation.ts +++ b/src/lib/validation/guess-check-validation.ts @@ -5,8 +5,11 @@ const guessCheckSchema = z.object({ userId: z.string({ required_error: 'User ID is required', }), - puzzleId: z.coerce.number({ - required_error: 'Puzzle ID is required', + date: z.string({ + required_error: 'Date is required', + }), + boardSize: z.coerce.number({ + required_error: 'Board size is required', }), guess: z.array(z.array(z.string())) }) diff --git a/src/lib/validation/hint-validation.ts b/src/lib/validation/hint-validation.ts index c094653..513f7ba 100644 --- a/src/lib/validation/hint-validation.ts +++ b/src/lib/validation/hint-validation.ts @@ -4,8 +4,11 @@ const hintParamsSchema = z.object({ userId: z.string({ required_error: 'User ID is required', }), - puzzleId: z.coerce.number({ - required_error: 'Puzzle ID is required', + date: z.string({ + required_error: 'Date is required', + }), + boardSize: z.coerce.number({ + required_error: 'Board size is required', }), x: z.coerce .number() @@ -22,7 +25,8 @@ export function validateHintParams(searchParams: URLSearchParams, userId: string } { const result = hintParamsSchema.safeParse({ userId, - puzzleId: searchParams.get('puzzleId'), + date: searchParams.get('date'), + boardSize: searchParams.get('boardSize'), x: searchParams.get('x'), y: searchParams.get('y') }); diff --git a/src/services/game/puzzle.ts b/src/services/game/puzzle.ts index ce7e172..6c5c44c 100644 --- a/src/services/game/puzzle.ts +++ b/src/services/game/puzzle.ts @@ -1,34 +1,17 @@ import { DailyPuzzle } from "@prisma/client"; import { prisma } from "@/lib"; +import NodeCache from "node-cache"; -const cachedBoardsByKey = new Map(); -const cachedBoardsById = new Map(); -const cacheExpiryTimers = new Map>(); +const boardCache = new NodeCache(); const normalizeDate = (date: string): string => new Date(date).toISOString().split('T')[0]; const getCacheKey = (date: string, boardSize: number): string => `${date}:${boardSize}`; -const getExpiryMillis = (date: string): number => { +const getExpirySeconds = (date: string): number => { const normalizedDate = normalizeDate(date); - const puzzleDate = new Date(`${normalizedDate}T00:00:00.000Z`); - const nextDay = new Date(puzzleDate); + const nextDay = new Date(`${normalizedDate}T00:00:00.000Z`); nextDay.setUTCDate(nextDay.getUTCDate() + 1); - return Math.max(0, nextDay.getTime() - Date.now()); -}; - -const scheduleCacheEviction = (cacheKey: string, puzzleId: number, ttl: number): void => { - const existingTimer = cacheExpiryTimers.get(cacheKey); - if (existingTimer) { - clearTimeout(existingTimer); - } - - const timer = setTimeout(() => { - cacheExpiryTimers.delete(cacheKey); - cachedBoardsByKey.delete(cacheKey); - cachedBoardsById.delete(puzzleId); - }, ttl); - - cacheExpiryTimers.set(cacheKey, timer); + return Math.max(0, Math.floor((nextDay.getTime() - Date.now()) / 1000)); }; export function fetchBoard(boardSize: number): { x: number, y: number }[] { @@ -69,83 +52,36 @@ export function fetchBoard(boardSize: number): { x: number, y: number }[] { } interface getCurrentBoardParams { - puzzleId?: number; boardSize?: number; date?: string; } -const cacheDailyPuzzle = (dailyPuzzle: DailyPuzzle): void => { - const normalizedDate = normalizeDate(dailyPuzzle.date.toISOString().split('T')[0]); - const cacheKey = getCacheKey(normalizedDate, dailyPuzzle.boardSize); - - cachedBoardsByKey.set(cacheKey, dailyPuzzle); - cachedBoardsById.set(dailyPuzzle.id, dailyPuzzle); - scheduleCacheEviction(cacheKey, dailyPuzzle.id, getExpiryMillis(normalizedDate)); -}; +export async function getCurrentBoard({ boardSize, date }: getCurrentBoardParams): Promise { + if (!date || !boardSize) { + throw new Error("Invalid date or boardSize"); + } -const getCachedCurrentBoard = async (date: string, boardSize: number): Promise => { const normalizedDate = normalizeDate(date); const cacheKey = getCacheKey(normalizedDate, boardSize); - const cached = cachedBoardsByKey.get(cacheKey); + const cached = boardCache.get(cacheKey); if (cached) { return cached; } const puzzleDate = new Date(`${normalizedDate}T00:00:00.000Z`); let dailyPuzzle = await prisma.dailyPuzzle.findUnique({ - where: { - date_boardSize: { - date: puzzleDate, - boardSize - } - } + where: { date_boardSize: { date: puzzleDate, boardSize } } }); if (!dailyPuzzle) { const board = fetchBoard(boardSize); dailyPuzzle = await prisma.dailyPuzzle.create({ - data: { - date: puzzleDate, - board, - boardSize - } + data: { date: puzzleDate, board, boardSize } }); } - cacheDailyPuzzle(dailyPuzzle); + boardCache.set(cacheKey, dailyPuzzle, getExpirySeconds(normalizedDate)); return dailyPuzzle; -}; - -const getCachedCurrentBoardById = async (puzzleId: number): Promise => { - const cached = cachedBoardsById.get(puzzleId); - if (cached) { - return cached; - } - - const dailyPuzzle = await prisma.dailyPuzzle.findUnique({ - where: { - id: puzzleId - } - }); - - if (!dailyPuzzle) { - throw new Error("Invalid puzzleId"); - } - - cacheDailyPuzzle(dailyPuzzle); - return dailyPuzzle; -}; - -export async function getCurrentBoard({ puzzleId, boardSize, date }: getCurrentBoardParams): Promise { - if (puzzleId) { - return getCachedCurrentBoardById(puzzleId); - } - - if (!date || !boardSize) { - throw new Error("Invalid date or boardSize"); - } - - return getCachedCurrentBoard(date, boardSize); } export function getAdjacentCount(board: { x: number, y: number }[], boardSize: number, x: number, y: number): number {