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