From f1799759e2ccb396da7ff5923caf5a0e5ca95b0a Mon Sep 17 00:00:00 2001 From: "J. Lewis" <6710419+lewxdev@users.noreply.github.com> Date: Sat, 17 Aug 2024 21:22:07 -0400 Subject: [PATCH 1/5] feat: use redis for sessions --- app/server.ts | 12 ++++++------ app/utils/redis.ts | 34 +++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/app/server.ts b/app/server.ts index c5262e3..1eaf5c3 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 }); @@ -24,8 +24,8 @@ async function main() { 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 +34,8 @@ async function main() { next(); }); - io.on("connection", (socket) => { - sessions.set(socket.data.sessionId, "alive"); + io.on("connection", async (socket) => { + await redis.startSession(socket.data.sessionId); socket.emit("sessionAlive", socket.data.sessionId); clientsCount++; @@ -45,7 +45,7 @@ async function main() { socket.on("expose", async (index) => { if (field.exposeCell(index) === "dead") { - sessions.set(socket.data.sessionId, "dead"); + await redis.killSession(socket.data.sessionId); socket.emit("sessionDead"); } else { field = await field.handleComplete(); diff --git a/app/utils/redis.ts b/app/utils/redis.ts index f2550ef..710d143 100644 --- a/app/utils/redis.ts +++ b/app/utils/redis.ts @@ -1,24 +1,40 @@ import { Redis } from "ioredis"; import _ from "lodash"; -const key = "field:data"; +const fieldKey = "field:data"; +const sessionKey = "user:sessions"; -function getClient() { - return new Redis(process.env["REDIS_URL"]!, { family: 6 }); -} +const redis = new Redis(process.env["REDIS_URL"]!, { family: 6 }); 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)); } 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(sessionId: string) { + const value = await redis.hget(sessionKey, sessionId); + return value === "alive" || value === "dead" ? value : null; +} + +export async function startSession(sessionId: string) { + return redis.hset(sessionKey, sessionId, "alive"); +} + +export async function killSession(sessionId: string) { + return redis.hset(sessionKey, sessionId, "dead"); +} + +export async function reviveSessions() { + const sessions = await redis.hkeys(sessionKey); + const args = sessions.flatMap((sessionId) => [sessionId, "alive"]); + redis.hset(sessionKey, ...args); +} From be9d9806778aaafe58f4d04de4453efcea09b476 Mon Sep 17 00:00:00 2001 From: "J. Lewis" <6710419+lewxdev@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:30:32 -0400 Subject: [PATCH 2/5] refactor: clean up some things --- app/components/socket-provider.tsx | 8 +++----- app/server.ts | 4 ++-- app/types/index.ts | 2 ++ app/utils/game.ts | 3 ++- app/utils/redis.ts | 16 ++++++---------- 5 files changed, 15 insertions(+), 18 deletions(-) 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 1eaf5c3..55705ba 100644 --- a/app/server.ts +++ b/app/server.ts @@ -35,7 +35,7 @@ async function main() { }); io.on("connection", async (socket) => { - await redis.startSession(socket.data.sessionId); + await redis.setSession(socket.data.sessionId, "alive"); socket.emit("sessionAlive", socket.data.sessionId); clientsCount++; @@ -45,7 +45,7 @@ async function main() { socket.on("expose", async (index) => { if (field.exposeCell(index) === "dead") { - await redis.killSession(socket.data.sessionId); + await redis.setSession(socket.data.sessionId, "dead"); socket.emit("sessionDead"); } else { field = await field.handleComplete(); 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..6b9b133 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"; @@ -56,7 +57,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); diff --git a/app/utils/redis.ts b/app/utils/redis.ts index 710d143..8aafd2d 100644 --- a/app/utils/redis.ts +++ b/app/utils/redis.ts @@ -1,5 +1,5 @@ import { Redis } from "ioredis"; -import _ from "lodash"; +import type { SessionState } from "@/types"; const fieldKey = "field:data"; const sessionKey = "user:sessions"; @@ -20,20 +20,16 @@ export async function encodeData(value: number[]) { return value; } -export async function getSession(sessionId: string) { - const value = await redis.hget(sessionKey, sessionId); +export async function getSession(id: string): Promise { + const value = await redis.hget(sessionKey, id); return value === "alive" || value === "dead" ? value : null; } -export async function startSession(sessionId: string) { - return redis.hset(sessionKey, sessionId, "alive"); +export async function setSession(id: string, state: SessionState) { + return redis.hset(sessionKey, id, state); } -export async function killSession(sessionId: string) { - return redis.hset(sessionKey, sessionId, "dead"); -} - -export async function reviveSessions() { +export async function resetSessions() { const sessions = await redis.hkeys(sessionKey); const args = sessions.flatMap((sessionId) => [sessionId, "alive"]); redis.hset(sessionKey, ...args); From c1d4bd483375bb8d81eccf88d0e54a5a1092c9ff Mon Sep 17 00:00:00 2001 From: "J. Lewis" <6710419+lewxdev@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:49:59 -0400 Subject: [PATCH 3/5] fix: usage of redis helpers --- app/utils/redis.ts | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/app/utils/redis.ts b/app/utils/redis.ts index 8aafd2d..8ba61c7 100644 --- a/app/utils/redis.ts +++ b/app/utils/redis.ts @@ -4,33 +4,42 @@ import type { SessionState } from "@/types"; const fieldKey = "field:data"; const sessionKey = "user:sessions"; -const redis = new Redis(process.env["REDIS_URL"]!, { family: 6 }); +const createHelper = + ( + callback: (redis: Redis, ...args: TArgs) => TReturn, + ) => + (...args: TArgs) => { + const redis = new Redis(process.env["REDIS_URL"]!, { family: 6 }); + return callback(redis, ...args); + }; -export async function decodeData() { +export const decodeData = createHelper(async (redis) => { const value = await redis.get(fieldKey); if (typeof value !== "string" || !value) { throw new Error(`unexpected data on key: ${fieldKey}`); } return Array.from(value, (char) => char.charCodeAt(0)); -} +}); -export async function encodeData(value: number[]) { +export const encodeData = createHelper(async (redis, value: number[]) => { const binaryString = Buffer.from(value).toString("binary"); await redis.set(fieldKey, binaryString); return value; -} +}); -export async function getSession(id: string): Promise { +export const getSession = createHelper(async (redis, id: string) => { const value = await redis.hget(sessionKey, id); return value === "alive" || value === "dead" ? value : null; -} +}); -export async function setSession(id: string, state: SessionState) { - return redis.hset(sessionKey, id, state); -} +export const setSession = createHelper( + async (redis, id: string, state: SessionState) => { + return redis.hset(sessionKey, id, state); + }, +); -export async function resetSessions() { +export const resetSessions = createHelper(async (redis) => { const sessions = await redis.hkeys(sessionKey); const args = sessions.flatMap((sessionId) => [sessionId, "alive"]); - redis.hset(sessionKey, ...args); -} + return redis.hset(sessionKey, ...args); +}); From efe43b1cc4c8d1bf1d4594da8f013c47dc1af1df Mon Sep 17 00:00:00 2001 From: "J. Lewis" <6710419+lewxdev@users.noreply.github.com> Date: Tue, 20 Aug 2024 22:02:34 -0400 Subject: [PATCH 4/5] refactor: okay, maybe a little more readable --- app/utils/redis.ts | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/app/utils/redis.ts b/app/utils/redis.ts index 8ba61c7..c4e6500 100644 --- a/app/utils/redis.ts +++ b/app/utils/redis.ts @@ -4,42 +4,40 @@ import type { SessionState } from "@/types"; const fieldKey = "field:data"; const sessionKey = "user:sessions"; -const createHelper = - ( - callback: (redis: Redis, ...args: TArgs) => TReturn, - ) => - (...args: TArgs) => { - const redis = new Redis(process.env["REDIS_URL"]!, { family: 6 }); - return callback(redis, ...args); - }; +function getClient() { + return new Redis(process.env["REDIS_URL"]!, { family: 6 }); +} -export const decodeData = createHelper(async (redis) => { +export async function decodeData() { + const redis = getClient(); const value = await redis.get(fieldKey); if (typeof value !== "string" || !value) { throw new Error(`unexpected data on key: ${fieldKey}`); } return Array.from(value, (char) => char.charCodeAt(0)); -}); +} -export const encodeData = createHelper(async (redis, value: number[]) => { +export async function encodeData(value: number[]) { + const redis = getClient(); const binaryString = Buffer.from(value).toString("binary"); await redis.set(fieldKey, binaryString); return value; -}); +} -export const getSession = createHelper(async (redis, id: string) => { +export async function getSession(id: string) { + const redis = getClient(); const value = await redis.hget(sessionKey, id); return value === "alive" || value === "dead" ? value : null; -}); +} -export const setSession = createHelper( - async (redis, id: string, state: SessionState) => { - return redis.hset(sessionKey, id, state); - }, -); +export async function setSession(id: string, state: SessionState) { + const redis = getClient(); + return redis.hset(sessionKey, id, state); +} -export const resetSessions = createHelper(async (redis) => { +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); -}); +} From 772e6d0e1b91fed7eb51585d78605e0c499075f9 Mon Sep 17 00:00:00 2001 From: "J. Lewis" <6710419+lewxdev@users.noreply.github.com> Date: Tue, 20 Aug 2024 22:30:40 -0400 Subject: [PATCH 5/5] fix: missing resetSessions --- app/server.ts | 10 ++++------ app/utils/game.ts | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/server.ts b/app/server.ts index 55705ba..7499314 100644 --- a/app/server.ts +++ b/app/server.ts @@ -18,9 +18,7 @@ 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; @@ -47,16 +45,16 @@ async function main() { if (field.exposeCell(index) === "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/utils/game.ts b/app/utils/game.ts index 6b9b133..36d8f50 100644 --- a/app/utils/game.ts +++ b/app/utils/game.ts @@ -29,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); @@ -78,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) {