import { db } from "~/server/db"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure, } from "~/server/api/trpc"; import { add } from "~/server/services/chat.service"; import { zChatId, zUserId } from "~/server/utils/zod_types"; import { ee, type OnlineStatus } from "~/server/sse"; import { getMessagesArray } from "~/utils/kysely-helper"; import { tracked, TRPCError } from "@trpc/server"; import { addTyping, getCurrentlyTyping, removeStaleTyping, removeTyping, } from "~/server/controllers/chat.controller"; import { getKeydbClient } from "~/utils/keydb"; export const chatSSERouter = createTRPCRouter({ list: publicProcedure.query(async () => { return await db .selectFrom("chats") .select([ "chats.chatid", "chats.created_at", "chats.user_ref", getMessagesArray(), ]) .innerJoin("users", "users.id", "chats.user_ref") .select(["users.username", "users.email", "users.nome"]) .orderBy("chats.created_at", "desc") .execute(); }), create: protectedProcedure .input(z.object({ userId: zUserId })) .mutation(async ({ input }) => { const newChat = await add({ db, userId: input.userId, }); return newChat.chatid; }), type: protectedProcedure .input( z.object({ chatId: zChatId, typing: z.boolean(), username: z.string() }), ) .mutation(async ({ ctx, input }) => { try { await removeStaleTyping(input.chatId); if (input.typing) { // Add the user to the set await addTyping({ chatId: input.chatId, userId: ctx.session.id, username: input.username, }); } else { await removeTyping({ chatId: input.chatId, userId: ctx.session.id, username: input.username, }); } ee.emit("chatUpdates", input.chatId, { eventType: "typing", data: await getCurrentlyTyping(input.chatId), }); return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred while processing the typing event: " + (e as Error).message, }); } }), enterChat: protectedProcedure .input(z.object({ chatId: zChatId })) .mutation(async ({ input, ctx }) => { const chatKey = `chat:${input.chatId}:onlineUsers`; const keydb = getKeydbClient(); // Add the user to the Redis set await keydb.sadd( chatKey, JSON.stringify({ userId: ctx.session.id, isAdmin: ctx.session.isAdmin, nome: ctx.session.nome, }), ); const onlineUsers = await keydb.smembers(chatKey); const parsedUsers = onlineUsers.map( (user) => JSON.parse(user) as OnlineStatus, ); ee.emit("chatUpdates", input.chatId, { eventType: "online", data: parsedUsers, }); return parsedUsers; }), leaveChat: protectedProcedure .input(z.object({ chatId: zChatId })) .mutation(async ({ input, ctx }) => { const chatKey = `chat:${input.chatId}:onlineUsers`; const keydb = getKeydbClient(); // Remove the user from the Redis set await keydb.srem( chatKey, JSON.stringify({ userId: ctx.session.id, isAdmin: ctx.session.isAdmin, nome: ctx.session.nome, }), ); const onlineUsers = await keydb.smembers(chatKey); const parsedUsers = onlineUsers.map( (user) => JSON.parse(user) as OnlineStatus, ); ee.emit("chatUpdates", input.chatId, { eventType: "online", data: parsedUsers, }); return parsedUsers; }), onChatEvent: protectedProcedure .input( z.object({ chatId: zChatId, }), ) .subscription(async function* ({ input, signal }) { try { const iterable = ee.toIterable("chatUpdates", { signal, }); for await (const [chatId, data] of iterable) { if (chatId === input.chatId) { yield tracked(chatId, data); } } } catch (e) { if (e instanceof Error && e.name === "AbortError") return; console.error("Error in onChatEvent subscription:", e); } }), });