Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use redis for sessions #29

Merged
merged 5 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions app/components/socket-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SocketContextValue | null>(null);
Expand All @@ -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<SessionState | null>(null);

useEffect(() => {
socket.auth = { sessionId: localStorage.getItem("sessionId") };
Expand Down
22 changes: 10 additions & 12 deletions app/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, "alive" | "dead">();

async function main() {
const app = next({ dev, hostname, port });
Expand All @@ -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
Expand All @@ -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++;
Expand All @@ -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);
});

Expand Down
2 changes: 2 additions & 0 deletions app/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 6 additions & 7 deletions app/utils/game.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down
29 changes: 24 additions & 5 deletions app/utils/redis.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
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 });
}

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(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() {
lewxdev marked this conversation as resolved.
Show resolved Hide resolved
const redis = getClient();
const sessions = await redis.hkeys(sessionKey);
const args = sessions.flatMap((sessionId) => [sessionId, "alive"]);
return redis.hset(sessionKey, ...args);
}