diff --git a/app/components/socket-provider.tsx b/app/components/socket-provider.tsx index ed89643..5a02f9a 100644 --- a/app/components/socket-provider.tsx +++ b/app/components/socket-provider.tsx @@ -2,13 +2,13 @@ import { createContext, useContext, useEffect, useState } from "react"; import { io } from "socket.io-client"; -import type { SocketClient } from "@/types"; +import type { SessionState, SocketClient } from "@/types"; const socket: SocketClient = io({ autoConnect: false }); type SocketContextValue = { socket: SocketClient; - sessionState: "alive" | "dead" | null; + sessionState: SessionState | null; }; const SocketContext = createContext(null); @@ -22,9 +22,7 @@ export function useSocket() { } export function SocketProvider({ children }: React.PropsWithChildren) { - const [sessionState, setSessionState] = useState<"alive" | "dead" | null>( - null, - ); + const [sessionState, setSessionState] = useState(null); useEffect(() => { socket.auth = { sessionId: localStorage.getItem("sessionId") }; diff --git a/app/server.ts b/app/server.ts index c5262e3..7499314 100644 --- a/app/server.ts +++ b/app/server.ts @@ -4,11 +4,11 @@ import next from "next"; import { Server } from "socket.io"; import type { SocketServer } from "@/types"; import { Field } from "@/utils/game"; +import * as redis from "@/utils/redis"; const dev = process.env.NODE_ENV !== "production"; const hostname = "localhost"; const port = 3000; -const sessions = new Map(); async function main() { const app = next({ dev, hostname, port }); @@ -18,14 +18,12 @@ async function main() { const io: SocketServer = new Server(httpServer); let clientsCount = 0; - let field = await Field.fromRedis(); - field = await field.handleComplete(); io.use(async (socket, next) => { const { sessionId } = socket.handshake.auth; - const sessionState = sessions.get(sessionId); - if (sessions.get(sessionId) === "dead") { + const sessionState = await redis.getSession(sessionId); + if (sessionState === "dead") { return next(new Error("dead")); } socket.data.sessionId = sessionState @@ -34,8 +32,8 @@ async function main() { next(); }); - io.on("connection", (socket) => { - sessions.set(socket.data.sessionId, "alive"); + io.on("connection", async (socket) => { + await redis.setSession(socket.data.sessionId, "alive"); socket.emit("sessionAlive", socket.data.sessionId); clientsCount++; @@ -45,18 +43,18 @@ async function main() { socket.on("expose", async (index) => { if (field.exposeCell(index) === "dead") { - sessions.set(socket.data.sessionId, "dead"); + await redis.setSession(socket.data.sessionId, "dead"); socket.emit("sessionDead"); - } else { - field = await field.handleComplete(); - io.emit("exposedPercent", field.exposedPercent); + } else if (field.isComplete) { + field = await Field.create(field.size + 10); + await redis.resetSessions(); } + io.emit("exposedPercent", field.exposedPercent); io.emit("update", field.plots); }); socket.on("flag", async (index) => { field.flagCell(index); - field = await field.handleComplete(); io.emit("update", field.plots); }); diff --git a/app/types/index.ts b/app/types/index.ts index b113572..310c904 100644 --- a/app/types/index.ts +++ b/app/types/index.ts @@ -2,6 +2,8 @@ import type { Server } from "socket.io"; import type { Socket } from "socket.io-client"; import type { Field } from "@/utils/game"; +export type SessionState = "alive" | "dead"; + type ClientToServerEvents = { expose(index: number): void; flag(index: number): void; diff --git a/app/utils/game.ts b/app/utils/game.ts index de67ea3..36d8f50 100644 --- a/app/utils/game.ts +++ b/app/utils/game.ts @@ -1,4 +1,5 @@ import _ from "lodash"; +import type { SessionState } from "@/types"; import * as redis from "@/utils/redis"; export type PlotState = number | "mine" | "unknown" | "flagged"; @@ -28,6 +29,10 @@ export class Field { ); } + public get isComplete() { + return this.data.every((byte) => isExposed(byte) || isMine(byte)); + } + public static async create(size = 10) { const area = size ** 2; const mineCount = Math.round(area * 0.15); @@ -56,7 +61,7 @@ export class Field { } } - public exposeCell(index: number) { + public exposeCell(index: number): SessionState { if (!_.isNil(this.data[index]) && !isExposed(this.data[index])) { // set exposed state bit, unset the flagged state bit this.data[index] = (this.data[index] | (1 << 5)) & ~(1 << 4); @@ -77,12 +82,6 @@ export class Field { this.data[index] ^= 1 << 4; } } - - public async handleComplete() { - return this.data.every((byte) => isExposed(byte) || isMine(byte)) - ? await Field.create(this.size + 10) - : this; - } } function getOffsets(size: number, index: number) { diff --git a/app/utils/redis.ts b/app/utils/redis.ts index f2550ef..c4e6500 100644 --- a/app/utils/redis.ts +++ b/app/utils/redis.ts @@ -1,7 +1,8 @@ import { Redis } from "ioredis"; -import _ from "lodash"; +import type { SessionState } from "@/types"; -const key = "field:data"; +const fieldKey = "field:data"; +const sessionKey = "user:sessions"; function getClient() { return new Redis(process.env["REDIS_URL"]!, { family: 6 }); @@ -9,9 +10,9 @@ function getClient() { export async function decodeData() { const redis = getClient(); - const value = await redis.get(key); + const value = await redis.get(fieldKey); if (typeof value !== "string" || !value) { - throw new Error(`unexpected data on key: ${key}`); + throw new Error(`unexpected data on key: ${fieldKey}`); } return Array.from(value, (char) => char.charCodeAt(0)); } @@ -19,6 +20,24 @@ export async function decodeData() { export async function encodeData(value: number[]) { const redis = getClient(); const binaryString = Buffer.from(value).toString("binary"); - await redis.set(key, binaryString); + await redis.set(fieldKey, binaryString); return value; } + +export async function getSession(id: string) { + const redis = getClient(); + const value = await redis.hget(sessionKey, id); + return value === "alive" || value === "dead" ? value : null; +} + +export async function setSession(id: string, state: SessionState) { + const redis = getClient(); + return redis.hset(sessionKey, id, state); +} + +export async function resetSessions() { + const redis = getClient(); + const sessions = await redis.hkeys(sessionKey); + const args = sessions.flatMap((sessionId) => [sessionId, "alive"]); + return redis.hset(sessionKey, ...args); +}