Skip to content

Commit

Permalink
fix: updates for dead sessions (#33)
Browse files Browse the repository at this point in the history
* fix: updates for dead sessions

* fix: remove extraneous assignment in redis.ts

Co-authored-by: Drew Radcliff <[email protected]>

---------

Co-authored-by: Drew Radcliff <[email protected]>
  • Loading branch information
lewxdev and drewradcliff authored Aug 23, 2024
1 parent a927686 commit c48ca39
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 58 deletions.
3 changes: 1 addition & 2 deletions app/components/field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ import { useVirtualizer } from "@tanstack/react-virtual";
import { Skull } from "lucide-react";
import { Fade } from "@/components/fade";
import { Plot } from "@/components/plot";
import { useSocket } from "@/components/socket-provider";
import { useSocketEvent } from "@/hooks/use-socket-event";

const GRID_SIZE = 2;
const GAP_SIZE = GRID_SIZE * 0.125;
const PX_PER_REM = 16;

export function Field() {
const { sessionState } = useSocket();
const [sessionState] = useSocketEvent("sessionState");
const [plots] = useSocketEvent("update");
const size = plots ? Math.sqrt(plots.length) : 0;

Expand Down
2 changes: 1 addition & 1 deletion app/components/plot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type Props = React.HTMLAttributes<HTMLDivElement> & {
};

export function Plot({ className, index, state, ...props }: Props) {
const { socket } = useSocket();
const socket = useSocket();
const longPressProps = useLongPress({
onLongPress: useCallback(() => {
if (state === "flagged" || state === "unknown") {
Expand Down
38 changes: 14 additions & 24 deletions app/components/socket-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,9 @@

import { createContext, useContext, useEffect, useState } from "react";
import { io } from "socket.io-client";
import type { SessionState, SocketClient } from "@/types";
import type { SocketClient } from "@/types";

const socket: SocketClient = io({ autoConnect: false });

type SocketContextValue = {
socket: SocketClient;
sessionState: SessionState | null;
};

const SocketContext = createContext<SocketContextValue | null>(null);
const SocketContext = createContext<SocketClient | null>(null);

export function useSocket() {
const socketContext = useContext(SocketContext);
Expand All @@ -22,29 +15,26 @@ export function useSocket() {
}

export function SocketProvider({ children }: React.PropsWithChildren) {
const [sessionState, setSessionState] = useState<SessionState | null>(null);
const [socket] = useState<SocketClient>(() =>
io({
auth: (callback) => {
callback({ sessionId: localStorage.getItem("sessionId") });
},
}),
);

useEffect(() => {
socket.auth = { sessionId: localStorage.getItem("sessionId") };
socket.connect();

socket.on("connect_error", (error) => {
if (error.message === "dead") {
setSessionState("dead");
}
});

socket.on("session", (sessionId) => {
socket.auth = { sessionId };
localStorage.setItem("sessionId", sessionId);
});

socket.on("sessionState", setSessionState);
}, []);
return () => {
socket.off();
};
}, [socket]);

return (
<SocketContext.Provider value={{ socket, sessionState }}>
{children}
</SocketContext.Provider>
<SocketContext.Provider value={socket}>{children}</SocketContext.Provider>
);
}
2 changes: 1 addition & 1 deletion app/hooks/use-socket-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useSocket } from "@/components/socket-provider";
import type { ServerToClientEvents } from "@/types";

export function useSocketEvent<K extends keyof ServerToClientEvents>(event: K) {
const { socket } = useSocket();
const socket = useSocket();
const [result, setResult] = useState<
Parameters<ServerToClientEvents[K]> | []
>([]);
Expand Down
53 changes: 28 additions & 25 deletions app/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import crypto from "crypto";
import { createServer } from "node:http";
import next from "next";
import { Server } from "socket.io";
Expand All @@ -13,48 +12,52 @@ const port = 3000;
async function main() {
const app = next({ dev, hostname, port });
await app.prepare();

const httpServer = createServer(app.getRequestHandler());
const io: SocketServer = new Server(httpServer);

let clientsCount = 0;
let field = await Field.fromRedis();

// middleware on incoming connection (see: https://socket.io/docs/v4/server-api/#serverusefn)
// handles "authentication" and manages *initial* session data
// note:
// * socket is not connected when this runs
// * only executed once per connection
io.use(async (socket, next) => {
const { sessionId } = socket.handshake.auth;
const sessionState = await redis.getSession(sessionId);
if (sessionState === "dead") {
return next(new Error("dead"));
}
socket.data.sessionId = sessionState
? sessionId
: crypto.randomBytes(8).toString("hex");
socket.data = await redis.getSession(sessionId);
next();
});

// client connects (or reconnects)
io.on("connection", async (socket) => {
if (field.isComplete) {
field = await Field.create(field.size + 10);
await redis.resetSessions();
io.emit("sessionState", "alive");
}
// update client count for all clients
io.emit("clientsCount", io.engine.clientsCount);

await redis.setSession(socket.data.sessionId, "alive");
// send initial data to newly connected client
socket.emit("session", socket.data.sessionId);
socket.emit("sessionState", "alive");

clientsCount++;
io.emit("clientsCount", clientsCount);
io.emit("exposedPercent", field.exposedPercent);
socket.emit("sessionState", socket.data.sessionState);
socket.emit("update", field.plots);
socket.emit("exposedPercent", field.exposedPercent);

// middleware on incoming event (see: https://socket.io/docs/v4/server-api/#socketusefn)
// handles *subsequent* listeners, blocking events from dead sessions
socket.use((event, next) => {
if (socket.data.sessionState === "dead") {
return next(new Error("dead", { cause: event }));
}
next();
});

// client event listeners

socket.on("expose", async (index) => {
if (field.exposeCell(index) === "dead") {
await redis.setSession(socket.data.sessionId, "dead");
socket.emit("sessionState", "dead");
// update session state and emit to client
socket.emit("sessionState", (socket.data.sessionState = "dead"));
} else if (field.isComplete) {
field = await Field.create(field.size + 10);
await redis.resetSessions();
// everyone is alive again 🎉
io.emit("sessionState", "alive");
}
io.emit("exposedPercent", field.exposedPercent);
Expand All @@ -66,9 +69,9 @@ async function main() {
io.emit("update", field.plots);
});

// client disconnects
socket.on("disconnect", () => {
clientsCount--;
io.emit("clientsCount", clientsCount);
io.emit("clientsCount", io.engine.clientsCount);
});
});

Expand Down
3 changes: 2 additions & 1 deletion app/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ export type ServerToClientEvents = {

type InterServerEvents = {};

type SocketData = {
export type SocketData = {
sessionId: string;
sessionState: SessionState;
};

export type SocketClient = Socket<ServerToClientEvents, ClientToServerEvents>;
Expand Down
14 changes: 10 additions & 4 deletions app/utils/redis.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import crypto from "node:crypto";
import { Redis } from "ioredis";
import type { SessionState } from "@/types";
import type { SessionState, SocketData } from "@/types";

const fieldKey = "field:data";
const sessionKey = "user:sessions";
Expand All @@ -24,10 +25,15 @@ export async function encodeData(value: number[]) {
return value;
}

export async function getSession(id: string) {
export async function getSession(sessionId: string): Promise<SocketData> {
const redis = getClient();
const value = await redis.hget(sessionKey, id);
return value === "alive" || value === "dead" ? value : null;
const sessionState = await redis.hget(sessionKey, sessionId);
if (sessionState === "alive" || sessionState === "dead") {
return { sessionId, sessionState };
}
const newSessionId = crypto.randomBytes(8).toString("hex");
await setSession(newSessionId, "alive");
return { sessionId: newSessionId, sessionState: "alive" };
}

export async function setSession(id: string, state: SessionState) {
Expand Down

0 comments on commit c48ca39

Please sign in to comment.