Skip to content

Commit

Permalink
♻️ Remove mongoose from ChatMessage
Browse files Browse the repository at this point in the history
  • Loading branch information
coyotte508 committed Apr 28, 2024
1 parent 1223120 commit 31b9f38
Show file tree
Hide file tree
Showing 15 changed files with 387 additions and 386 deletions.
13 changes: 12 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@ module.exports = {
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/explicit-module-boundary-types": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-empty-interfaces": "off",
Expand Down
8 changes: 6 additions & 2 deletions apps/api/app/config/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { LockManager } from "mongo-locks";
import { migrate } from "../models/migrations";
import env from "./env";
import { MongoClient } from "mongodb";
import { createApiErrorCollection, createChatMessageCollection } from "@bgs/models";

const client = new MongoClient(env.database.bgs.url, { directConnection: true, ignoreUndefined: true });

const db = client.db(env.database.bgs.name);

const locks = new LockManager(db.collection("mongo-locks"));
export const locks = new LockManager(db.collection("mongo-locks"));

await db
.listCollections()
Expand All @@ -17,7 +18,10 @@ await db
console.log("Connected to database");
});

export const collections = {};
export const collections = {
apiErrors: await createApiErrorCollection(db),
chatMessages: await createChatMessageCollection(db),
};

if (!env.isTest && cluster.isMaster) {
await using lock = await locks.lock("db");
Expand Down
11 changes: 0 additions & 11 deletions apps/api/app/models/chatmessage.ts

This file was deleted.

1 change: 0 additions & 1 deletion apps/api/app/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from "./apierror";
export * from "./chatmessage";
export * from "./game";
export * from "./gameinfo";
export * from "./gamenotification";
Expand Down
115 changes: 61 additions & 54 deletions apps/api/app/routes/game/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,17 @@ import { timerDuration } from "@bgs/utils/time";
import assert from "assert";
import { addDays } from "date-fns";
import createError from "http-errors";
import { Context } from "koa";
import type { Context } from "koa";
import Router from "koa-router";
import { omit } from "lodash";
import locks from "mongo-locks";
import {
ChatMessage,
Game,
GameInfo,
GameNotification,
GamePreferences,
RoomMetaData,
RoomMetaDataDocument,
User,
} from "../../models";
import type { RoomMetaDataDocument } from "../../models";
import { Game, GameInfo, GameNotification, GamePreferences, RoomMetaData, User } from "../../models";
import { notifyGameStart } from "../../services/game";
import { isAdmin, isConfirmed, loggedIn } from "../utils";
import listings from "./listings";
import { ObjectId } from "mongodb";
import { z } from "zod";
import { collections, locks } from "../../config/db";

const router = new Router<Application.DefaultState, Context>();

Expand Down Expand Up @@ -232,23 +226,29 @@ router.post("/:gameId/chat", loggedIn, isConfirmed, async (ctx) => {
(ctx.state.user && ctx.state.game.players.some((pl) => pl._id.equals(ctx.state.user._id))),
"You must be a player of the game to chat!"
);
assert(ctx.request.body.type === "text" || ctx.request.body.type === "emoji");

const text = ctx.request.body?.data?.text?.trim();
assert(text, "Empty chat message");

const doc = new ChatMessage({
const parsed = z
.object({
type: z.enum(["text", "emoji"]),
data: z.object({
text: z.string().min(1).max(300).trim(),
}),
})
.parse(ctx.request.body);

await collections.chatMessages.insertOne({
_id: new ObjectId(),
room: ctx.state.game._id,
author: {
_id: ctx.state.user._id,
name: ctx.state.user.account.username,
},
data: {
text: ctx.request.body.data.text,
text: parsed.data.text,
},
type: ctx.request.body.type,
type: parsed.type,
});
await doc.save();

ctx.status = 200;
});

Expand Down Expand Up @@ -446,40 +446,47 @@ router.post("/:gameId/cancel", loggedIn, async (ctx) => {
"You must be a player of the game to vote!"
);

const free = await locks.lock("game-cancel", ctx.params.gameId);
await using lock = await locks.lock(["game-cancel", ctx.params.gameId]);

try {
const game = await Game.findOne({ _id: ctx.params.gameId });
if (!lock) {
throw createError(409, "The game is already being cancelled");
}

assert(game, createError(404));
assert(game.status === "active", "The game is not ongoing");
const game = await Game.findOne({ _id: ctx.params.gameId });

assert(game, createError(404));
assert(game.status === "active", "The game is not ongoing");

const player = game.players.find((pl) => pl._id.equals(ctx.state.user._id));
const player = game.players.find((pl) => pl._id.equals(ctx.state.user._id));

assert(!player.voteCancel, "You already voted to cancel the game");
assert(!player.voteCancel, "You already voted to cancel the game");

player.voteCancel = true;
await ChatMessage.create({
player.voteCancel = true;

await collections.chatMessages.insertOne({
_id: new ObjectId(),
room: game._id,
type: "system",
data: { text: `${ctx.state.user.account.username} voted to cancel this game` },
});

if (game.players.every((pl) => pl.voteCancel || pl.dropped)) {
await collections.chatMessages.insertOne({
_id: new ObjectId(),
room: game._id,
type: "system",
data: { text: `${player.name} voted to cancel this game` },
data: { text: `Game cancelled` },
});
game.status = "ended";
game.cancelled = true;
game.currentPlayers = null;
}

if (game.players.every((pl) => pl.voteCancel || pl.dropped)) {
await ChatMessage.create({ room: game._id, type: "system", data: { text: `Game cancelled` } });
game.status = "ended";
game.cancelled = true;
game.currentPlayers = null;
}

await game.save();
await game.save();

if (game.status === "ended") {
// Possible concurrency issue if game is cancelled at the exact same time as being finished
await GameNotification.create({ kind: "gameEnded", game: game._id });
}
} finally {
free().catch(console.error);
if (game.status === "ended") {
// Possible concurrency issue if game is cancelled at the exact same time as being finished
await GameNotification.create({ kind: "gameEnded", game: game._id });
}

ctx.status = 200;
Expand All @@ -491,21 +498,21 @@ router.post("/:gameId/quit", loggedIn, async (ctx) => {
"You must be a player of the game to quit!"
);

const free = await locks.lock("game-cancel", ctx.params.gameId);
await using lock = await locks.lock(["game-cancel", ctx.params.gameId]);

try {
const game = await Game.findOne({ _id: ctx.params.gameId }).select("players status").lean(true);
if (!lock) {
throw createError(409, "The game is already being cancelled");
}

assert(game.status === "active", "The game is not ongoing");
const game = await Game.findOne({ _id: ctx.params.gameId }).select("players status").lean(true);

const player = game.players.find((pl) => pl._id.equals(ctx.state.user._id));
assert(game.status === "active", "The game is not ongoing");

assert(!player.quit && !player.dropped, "You already quit the game");
const player = game.players.find((pl) => pl._id.equals(ctx.state.user._id));

await GameNotification.create({ kind: "playerQuit", user: ctx.state.user._id, game: game._id });
} finally {
free().catch(console.error);
}
assert(!player.quit && !player.dropped, "You already quit the game");

await GameNotification.create({ kind: "playerQuit", user: ctx.state.user._id, game: game._id });

ctx.status = 200;
});
Expand Down
99 changes: 55 additions & 44 deletions apps/api/app/services/game.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { subHours, subWeeks } from "date-fns";
import { shuffle } from "lodash";
import locks from "mongo-locks";
import { ChatMessage, Game, GameDocument, GameNotification } from "../models";
import type { GameDocument } from "../models";
import { Game, GameNotification } from "../models";
import { collections, locks } from "../config/db";
import { ObjectId } from "mongodb";

export async function notifyGameStart(game: GameDocument) {
export async function notifyGameStart(game: GameDocument): Promise<void> {
if (game.options.setup.playerOrder === "random") {
// Mongoose (5.10.0) will bug if I directly set to the shuffled value (because array item's .get are not set)
const shuffled = shuffle(game.players);
Expand All @@ -12,7 +14,13 @@ export async function notifyGameStart(game: GameDocument) {
await game.save();
}

await ChatMessage.create({ room: game._id, type: "system", data: { text: "Game started" } });
await collections.chatMessages.insertOne({
_id: new ObjectId(),
room: game._id,
type: "system",
data: { text: "Game started" },
});

await GameNotification.create({ game: game._id, kind: "gameStarted", processed: false });
}

Expand Down Expand Up @@ -41,38 +49,39 @@ export async function cancelOldOpenGames() {
});
}

export async function processSchedulesGames() {
const free = await locks.lock("game", "scheduled-games");
export async function processSchedulesGames(): Promise<void> {
await using lock = await locks.lock(["game", "scheduled-games"]);

try {
for await (const game of Game.find({
status: "open",
"options.timing.scheduledStart": { $lt: new Date() },
}).cursor()) {
const g: GameDocument = game;
if (!lock) {
return;
}

if (!g.ready) {
await ChatMessage.create({
room: game._id,
type: "system",
data: { text: "Game cancelled because it's not fully ready at scheduled start date" },
});
g.cancelled = true;
g.status = "ended";
await g.save();
continue;
}
// Do this to avoid being caught in a loop again, before game server starts the game
g.options.timing.scheduledStart = undefined;
for await (const game of Game.find({
status: "open",
"options.timing.scheduledStart": { $lt: new Date() },
}).cursor()) {
const g: GameDocument = game;

if (!g.ready) {
await collections.chatMessages.insertOne({
_id: new ObjectId(),
room: g._id,
type: "system",
data: { text: "Game cancelled because host didn't set the final options in time" },
});
g.cancelled = true;
g.status = "ended";
await g.save();
await notifyGameStart(g);
continue;
}
} finally {
free().catch(console.error);
// Do this to avoid being caught in a loop again, before game server starts the game
g.options.timing.scheduledStart = undefined;
await g.save();
await notifyGameStart(g);
}
}

export async function processUnreadyGames() {
export async function processUnreadyGames(): Promise<void> {
const games = await Game.find(
{
ready: false,
Expand All @@ -84,22 +93,24 @@ export async function processUnreadyGames() {

for (const toFetch of games) {
try {
const free = await locks.lock("game", toFetch._id);
try {
const game = await Game.findById(toFetch._id, { status: 1 });
await using lock = await locks.lock(["game", toFetch._id]);

if (!lock) {
continue;
}

if (game.status === "open") {
await ChatMessage.create({
room: game._id,
type: "system",
data: { text: "Game cancelled because host didn't set the final options in time" },
});
game.cancelled = true;
game.status = "ended";
await game.save();
}
} finally {
free().catch(console.error);
const game = await Game.findById(toFetch._id, { status: 1 });

if (game.status === "open") {
await collections.chatMessages.insertOne({
_id: new ObjectId(),
room: game._id,
type: "system",
data: { text: "Game cancelled because host didn't set the final options in time" },
});
game.cancelled = true;
game.status = "ended";
await game.save();
}
} catch (err) {
console.error(err);
Expand Down
Loading

0 comments on commit 31b9f38

Please sign in to comment.