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 { combineRedisIterators, redisAsyncIterator, redisPublish, } from "@/server/redis/sse-redis"; import { tracked, TRPCError } from "@trpc/server"; import { trcpSubscriptionInput } from "@/lib/validations/trcp"; import type { LobbyMemberLeaveEventData } from "@/server/redis/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"); await ctx.db.insert(lobbyMembers).values({ lobbyId: lobby.id, playerId: ctx.session.user.id, isReady: false, role: "admin", }); 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) redisPublish("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) redisPublish("lobby:member:join", { ...member, player }); 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) redisPublish("lobby:member:leave", { playerId: member.playerId }); 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) redisPublish("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) redisPublish("lobby:member:leave", { playerId: member.playerId, kicked: true, }); return member; }), // subscriptions onUpdate: publicProcedure .input(trcpSubscriptionInput) .subscription(async function* (opts) { if (opts.input?.lastEventId) { // fetch posts from a database that were missed. } for await (const lobby of redisAsyncIterator("lobby:update")) { yield tracked(lobby?.updatedAt?.toString(), { lobby, }); } }), onMemberUpdate: publicProcedure .input(trcpSubscriptionInput) .subscription(async function* (opts) { if (opts.input?.lastEventId) { // fetch posts from a database that were missed. } for await (const { event, data: membership } of combineRedisIterators([ "lobby:member:join", "lobby:member:leave", "lobby:member:update", ])) { switch (event) { case "lobby:member:join": yield tracked(String(membership.playerId), { joined: true, membership, }); break; case "lobby:member:leave": const data = membership as LobbyMemberLeaveEventData; yield tracked(String(membership.playerId), { joined: false, membership: data, }); break; // case "lobby:member:update": // yield tracked(String(membership.playerId), { // membership, // }); // break; } } }), });