297 lines
7.6 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 {
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;
}
}
}),
});