node-mongo-demo
node.js and mongodb demo
git clone https://9o.is/git/node-mongo-demo.git
commit 19c0d80accae6189e456f4a6e463fd38876cf8d1 parent 939669ac30edc32952cd6ac19254bd0e11e3994e Author: Jul <jul@9o.is> Date: Mon, 27 Jan 2025 14:35:23 -0500 simplify events logic with mongodb change streams Diffstat:
| A | backend/src/api/lucky7-bet-events.js | | | 83 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | backend/src/api/lucky7-bets-events.js | | | 61 | ------------------------------------------------------------- |
| M | backend/src/api/lucky7.js | | | 4 | ++-- |
| M | frontend/src/api/index.js | | | 2 | +- |
| M | frontend/src/components/Lucky7.js | | | 124 | ++++++++++++++++++++++++++++++------------------------------------------------- |
| M | frontend/src/index.css | | | 17 | +++++++++++++++-- |
6 files changed, 148 insertions(+), 143 deletions(-)
diff --git a/backend/src/api/lucky7-bet-events.js b/backend/src/api/lucky7-bet-events.js @@ -0,0 +1,83 @@ +import Lucky7Bet from "../models/lucky7-bet.js"; +import EventStream from "../utils/event-stream.js"; +import { dateDifferenceInSeconds } from "../services/lucky7-session.js"; + +const users = new Map(); +const bets = new Map(); + +Lucky7Bet.watch().on('change', data => { + const { + operationType, + documentKey: { _id } + } = data; + const id = _id.toString(); + + if (operationType === 'insert') { + const bet = data.fullDocument; + const { userId, rollAt, lucky, roll, win } = bet; + const user = users.get(userId.toString()); + const timeout = 1000 * dateDifferenceInSeconds(Date.now(), rollAt); + + user.nextBetTimeoutId = setTimeout(async () => { + user.stream.writeJson({ + operationType: 'update', + bet: { + id, + roll, + win, + state: 'finished', + }, + }); + + bets.delete(id); + delete user['nextBetTimeoutId']; + }, timeout) + + if (user) { + bets.set(id, bet); + user.stream.writeJson({ + operationType, + bet: { + id, + rollAt, + lucky, + state: 'pending', + }, + }) + } + + } else if (operationType === 'update') { + const bet = bets.get(id); + const userId = bet?.userId.toString(); + const user = userId ? users.get(userId) : undefined; + + if (bet && user) { + const { lucky, win } = data.updateDescription.updatedFields; + user.stream.writeJson({ + operationType, + bet: { + id, + lucky, + state: 'pending', + }, + }); + } + } +}); + +const lucky7BetEvents = async (req, res) => { + const { userId } = req; + const stream = new EventStream(req, res); + + users.set(userId, { stream }); + + stream.onClose(() => { + const user = users.get(userId) + if (user?.nextGameTimeout) { + clearTimeout(user.nextGameTimeout); + } + users.delete(userId); + }); +}; + +export default lucky7BetEvents; diff --git a/backend/src/api/lucky7-bets-events.js b/backend/src/api/lucky7-bets-events.js @@ -1,61 +0,0 @@ -import Lucky7Bet from "../models/lucky7-bet.js"; -import EventStream from "../utils/event-stream.js"; -import { dateDifferenceInSeconds } from "../services/lucky7-session.js"; - -const lucky7BetsEvents = async (req, res) => { - const { id } = req.params; - const { userId } = req; - - try { - const bet = await Lucky7Bet.findById(id).exec(); - - if (!bet) { - return res.status(404).json({ message: "Bet does not exist" }); - } - - const pending = { - id: bet.id, - rollAt: bet.rollAt, - lucky: bet.lucky, - state: "pending", - }; - - const finished = { - ...pending, - roll: bet.roll, - win: bet.win, - state: "finished", - }; - - const timeout = 1000 * dateDifferenceInSeconds(new Date(), bet.rollAt); - - if (timeout <= 0) { - return res.status(200).json(finished); - } - - const stream = new EventStream(req, res); - - const timeoutId = setTimeout(async () => { - const bet = await Lucky7Bet.findById(id).exec(); - - stream.writeJson({ - id: bet.id, - rollAt: bet.rollAt, - lucky: bet.lucky, - roll: bet.roll, - win: bet.win, - state: "finished", - }); - - stream.close(); - }, timeout); - - stream.writeJson(pending); - stream.onClose(() => clearTimeout(timeoutId)); - } catch (error) { - console.error(error); - res.status(500).json({ message: "Something went wrong" }); - } -}; - -export default lucky7BetsEvents; diff --git a/backend/src/api/lucky7.js b/backend/src/api/lucky7.js @@ -1,7 +1,7 @@ import express from "express"; import lucky7BetsCreate from "./lucky7-bets-create.js"; import lucky7Leaderboard from "./lucky7-leaderboard.js"; -import lucky7BetsEvents from "./lucky7-bets-events.js"; +import lucky7BetEvents from "./lucky7-bet-events.js"; import lucky7SessionsCreate from "./lucky7-sessions-create.js"; import auth from "../utils/auth.js"; @@ -9,7 +9,7 @@ const router = express.Router(); router.post("/sessions", auth, lucky7SessionsCreate); router.post("/bets", auth, lucky7BetsCreate); -router.get("/bets/:id/events", auth, lucky7BetsEvents); +router.get("/betEvents", auth, lucky7BetEvents); router.get("/leaderboard", lucky7Leaderboard); export default router; diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js @@ -18,7 +18,7 @@ export const changePassword = (formData) => export const lucky7Leaderboard = () => API.post("/api/lucky7/leaderboard") export const lucky7Session = () => API.post("/api/lucky7/sessions") export const lucky7Bet = lucky => API.post("/api/lucky7/bets", { lucky }); -export const lucky7BetEvents = id => API.get(`/api/lucky7/bets/${id}/events`, { +export const lucky7BetEvents = () => API.get(`/api/lucky7/betEvents`, { headers: { 'Accept': 'text/event-stream', }, diff --git a/frontend/src/components/Lucky7.js b/frontend/src/components/Lucky7.js @@ -7,7 +7,7 @@ const useSession = () => { React.useEffect(() => { const func = async () => { const response = await api.lucky7Session(); - setSession(response.data) + setSession(response.data); }; func(); }, []); @@ -17,13 +17,12 @@ const useSession = () => { }; }; -const useBetEvents = (id, onEvent) => { +const useBetEvents = onEvent => { const [listening, setListening] = React.useState(false); - const [events, setEvents] = React.useState([]); React.useEffect(() => { const connect = async () => { - const response = await api.lucky7BetEvents(id); + const response = await api.lucky7BetEvents(); const stream = response.data; const reader = stream.pipeThrough(new TextDecoderStream()).getReader(); @@ -35,49 +34,11 @@ const useBetEvents = (id, onEvent) => { break; } const json = JSON.parse(value.slice(5)); - setEvents((events) => [...events, json]) onEvent(json); } }; - if (onEvent && !listening) connect(); - }, [id, onEvent]); - - return { - listening, - events, - }; -}; - -const Lucky7Bet = props => { - const { - onGameEvent, - id, - lucky, - rollAt, - roll, - win, - } = props; - - const { listening, events } = useBetEvents(id, onGameEvent) - - return ( - <> - id: {id}<br/> - Lucky: {String(lucky)}<br/> - rollAt: {rollAt}<br/> - dice roll: {roll?.[0]} {roll?.[1]}<br/> - win: {String(win ?? "")}<br/> - {onGameEvent ? ( - <> - <h4>Events</h4> - Listening: {String(listening)}<br/> - <ol> - {events.map((e, i) => <li key={i}>{ JSON.stringify(e) }</li>)} - </ol> - </> - ) : null} - </> - ); + if (!listening) connect(); + }, [listening, onEvent]); }; const getNextGameTime = (createdAt) => { @@ -127,55 +88,64 @@ const Timer = ({ createdAt }) => { ); }; +const sortBets = bets => + Object.values(bets).sort((a, b) => a.rollAt < b.rollAt ? 1 : -1); + const Lucky7 = user => { const { session } = useSession(); - const [status, setStatus] = React.useState(undefined); - const [nextGameBet, setNextGameBet] = React.useState(undefined); - const [lastGameBet, setLastGameBet] = React.useState(undefined); + const [bets, setBets] = React.useState({}); + const [error, setError] = React.useState(undefined); + + useBetEvents(e => { + const { bet } = e; + setBets(bets => ({ ...bets, [bet.id]: { ...bets[bet.id], ...bet } })); + setError(undefined); + }); const handleBet = lucky => async () => { try { - const response = await api.lucky7Bet(lucky); - setNextGameBet(response.data); - setStatus(undefined); + await api.lucky7Bet(lucky); + setError(undefined); } catch (error) { if (error.status === 400 && error.response.data.code === "TIMER") { - setStatus(error.response.data.message); + setError(error.response.data.message); } else { console.error(error) } } }; - const onGameEvent = bet => { - if (bet.state === "finished") { - setStatus(undefined); - setNextGameBet(undefined); - setLastGameBet(bet) - } - }; - return ( <> - {session ? <Timer createdAt={session.createdAt} /> : null} - <button onClick={handleBet(true)}>Lucky</button> - <button onClick={handleBet(false)}>Unlucky</button> - {status ? <p>{status}</p> : null} - {nextGameBet ? ( - <> - <h3>Next Game Bet</h3> - <Lucky7Bet onGameEvent={onGameEvent} {...nextGameBet} /> - </> - ) : null} - {lastGameBet ? ( - <> - <h3>Last Game Bet</h3> - <Lucky7Bet {...lastGameBet} /> - </> - ) : null} - <h3>Session</h3> - <p>{JSON.stringify(session)}</p> + {error ? <div>{error}</div> : <> </>} + <div> + <button onClick={handleBet(true)}>Lucky</button> + <button onClick={handleBet(false)}>Unlucky</button> + {session ? <Timer createdAt={session.createdAt} /> : null} + </div> + <table> + <thead> + <tr> + <th>state</th> + <th>rollAt</th> + <th>lucky</th> + <th>roll</th> + <th>win</th> + </tr> + </thead> + <tbody> + {sortBets(bets).map(bet => ( + <tr key={bet.id}> + <td>{bet.state}</td> + <td>{bet.rollAt}</td> + <td>{String(bet.lucky ?? "")}</td> + <td>{(bet.roll || []).join(',')}</td> + <td>{String(bet.win ?? "")}</td> + </tr> + ))} + </tbody> + </table> </> ); }; diff --git a/frontend/src/index.css b/frontend/src/index.css @@ -1,3 +1,17 @@ body { background-color: #e9e9e9; -} -\ No newline at end of file +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + padding: 2px; + text-align: center; +} + +tbody > tr:first-child { + background-color: rgb(190, 250, 230); +}