326 lines
8.2 KiB
TypeScript

import { z } from "zod";
import { lobbies, lobbyMembers, players, type Lobby } from "@/server/db/schema";
import {
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "@/server/api/trpc";
import {
LobbyMemberRoleSchema,
lobbyPatchSchema,
} from "@/lib/validations/lobby";
import { and, eq } from "drizzle-orm";
import { tracked } from "@trpc/server";
import type { EventArgs, SSE_EVENTS } from "@/server/events";
import EventEmitter, { on } from "node:events";
import {
decreaseLobbyMemberCount,
increaseLobbyMemberCount,
handleLobbyAfterLeave,
} from "@/server/api/mutation-utils/lobby-utils";
type EventMap<T> = Record<keyof T, any[]>;
class IterableEventEmitter<T extends EventMap<T>> extends EventEmitter<T> {
toIterable<TEventName extends keyof T & string>(
eventName: TEventName,
opts?: NonNullable<Parameters<typeof on>[2]>,
): AsyncIterable<T[TEventName]> {
return on(this as any, eventName, opts) as any;
}
}
export const ee = new IterableEventEmitter<SSE_EVENTS>();
export const lobbyRouter = createTRPCRouter({
// queries
getCurrentLobby: protectedProcedure.query(async ({ ctx }) => {
const reuslt = await ctx.db.query.lobbyMembers.findFirst({
where: eq(lobbyMembers.playerId, ctx.session.user.id),
with: {
lobby: {
with: {
members: {
with: {
player: true,
},
},
},
},
},
});
return reuslt?.lobby!;
}),
get: publicProcedure
.input(
z.object({
id: z.string(),
}),
)
.query(
async ({ ctx, input }) =>
await ctx.db.query.lobbies.findFirst({
where: eq(lobbies.id, input.id),
with: {
members: {
with: {
player: true,
},
},
},
}),
),
// mutations
create: protectedProcedure
.input(
z.object({
lobby: lobbyPatchSchema,
}),
)
.mutation(async ({ ctx, input }) => {
const [lobby] = await ctx.db
.insert(lobbies)
.values({
...input.lobby,
createdById: ctx.session.user.id,
})
.returning({ id: lobbies.id });
if (!lobby) throw new Error("Error creating lobby");
const [member] = await ctx.db
.insert(lobbyMembers)
.values({
lobbyId: lobby.id,
playerId: ctx.session.user.id,
isReady: false,
role: "admin",
})
.returning({
id: lobbyMembers.playerId,
});
if (member) increaseLobbyMemberCount(lobby.id);
return lobby;
}),
update: protectedProcedure
.input(
z.object({
lobby: lobbyPatchSchema,
lobbyId: z.string(),
}),
)
.mutation(async ({ ctx, input }) => {
console.log("Check if user is admin");
const [lobby] = await ctx.db
.update(lobbies)
.set(input.lobby)
.where(
and(
eq(lobbies.id, input.lobbyId),
eq(lobbies.createdById, ctx.session.user.id),
),
)
.returning();
if (lobby) ee.emit("lobby:update", lobby);
return lobby;
}),
delete: protectedProcedure
.input(
z.object({
lobbyId: z.string(),
}),
)
.mutation(async ({ ctx, input }) => {
console.log("Check if user is admin");
const [lobby] = await ctx.db
.delete(lobbies)
.where(
and(
eq(lobbies.id, input.lobbyId),
eq(lobbies.createdById, ctx.session.user.id),
),
)
.returning({ id: lobbies.id });
return lobby;
}),
membership: protectedProcedure
.input(
z.object({
lobbyId: z.string(),
join: z.boolean(),
}),
)
.mutation(async ({ ctx, input }) => {
if (input.join) {
try {
const [member] = await ctx.db
.insert(lobbyMembers)
.values({
lobbyId: input.lobbyId,
playerId: ctx.session.user.id,
isReady: false,
role: "player",
})
.returning();
const player = member
? await ctx.db.query.players.findFirst({
where: eq(players.id, member.playerId),
})
: undefined;
if (member) {
ee.emit("lobby:member:membership", input.lobbyId, true, {
...member,
player,
});
increaseLobbyMemberCount(input.lobbyId);
return { success: true, member };
}
} catch (e: unknown) {
if (e instanceof Error) {
if (
e.message.includes(
"duplicate key value violates unique constraint",
)
)
return {
knownError: true,
succes: false,
error: "You can only be in one lobby at a time.",
};
} else {
return {
knownError: true,
succes: false,
error: "Error joining lobby",
};
}
}
} else {
const [member] = await ctx.db
.delete(lobbyMembers)
.where(
and(
eq(lobbyMembers.lobbyId, input.lobbyId),
eq(lobbyMembers.playerId, ctx.session.user.id),
),
)
.returning();
if (member) {
ee.emit(
"lobby:member:membership",
input.lobbyId,
false,
member.playerId,
);
decreaseLobbyMemberCount(input.lobbyId);
handleLobbyAfterLeave(input.lobbyId);
return { success: true, member };
}
}
}),
// admin mutaions
changeRole: protectedProcedure
.input(
z.object({
lobbyId: z.string(),
playerId: z.string(),
role: LobbyMemberRoleSchema,
}),
)
.mutation(async ({ ctx, input }) => {
console.log("Check if user is admin");
const [member] = await ctx.db
.update(lobbyMembers)
.set(input.role === "admin" ? { role: "admin" } : { role: "player" })
.where(
and(
eq(lobbyMembers.lobbyId, input.lobbyId),
eq(lobbyMembers.playerId, input.playerId),
),
)
.returning();
if (member) ee.emit("lobby:member:update", member);
return member;
}),
kick: protectedProcedure
.input(
z.object({
lobbyId: z.string(),
playerId: z.string(),
}),
)
.mutation(async ({ ctx, input }) => {
console.log("Check if user is admin");
const [member] = await ctx.db
.delete(lobbyMembers)
.where(
and(
eq(lobbyMembers.lobbyId, input.lobbyId),
eq(lobbyMembers.playerId, input.playerId),
),
)
.returning();
if (member)
ee.emit(
"lobby:member:membership",
input.lobbyId,
false,
member.playerId,
true,
);
decreaseLobbyMemberCount(input.lobbyId);
return member;
}),
// subscriptions
onUpdate: publicProcedure
.input(z.object({ lobbyId: z.string() }))
.subscription(async function* (opts) {
const iterable = ee.toIterable("lobby:update", {
signal: opts.signal,
});
function* maybeYield([lobby, deleted]: EventArgs["lobby:update"]) {
if (lobby.id !== opts.input.lobbyId) {
return;
}
yield tracked(lobby.id, { lobby, deleted });
}
for await (const args of iterable) {
yield* maybeYield(args);
}
}),
onMemberUpdate: publicProcedure
.input(z.object({ lobbyId: z.string() }))
.subscription(async function* (opts) {
const iterable = ee.toIterable("lobby:member:membership", {
signal: opts.signal,
});
function* maybeYield([
lobbyId,
joined,
member,
kicked,
]: EventArgs["lobby:member:membership"]) {
if (lobbyId !== opts.input.lobbyId) {
return;
}
yield tracked(lobbyId, { member, joined, kicked });
}
for await (const args of iterable) {
yield* maybeYield(args);
}
}),
});