slidemap / src /lib /session-manager.ts
Nanny7's picture
Fix: TypeScript compilation errors
22009fe
Raw
History Blame Contribute Delete
20.1 kB
// ═══════════════════════════════════════════════════════════════════════════
// SESSION MANAGER - In-Memory Session State Management
// ═══════════════════════════════════════════════════════════════════════════
import { v4 as uuidv4 } from 'uuid';
import {
Session,
User,
ChatMessage,
Poll,
Doubt,
Reaction,
ReactionType,
WhiteboardStroke,
SSEEvent,
PresenceUser,
PresenceUpdate
} from './types';
import { storageService } from './storage';
// ═══════════════════════════════════════════════════════════════════════════
// IN-MEMORY STORES
// ═══════════════════════════════════════════════════════════════════════════
class SessionManager {
private sessions: Map<string, Session> = new Map();
private users: Map<string, User> = new Map();
private sessionUsers: Map<string, Set<string>> = new Map(); // sessionId -> userIds
private polls: Map<string, Map<string, Poll>> = new Map(); // sessionId -> pollId -> Poll
private doubts: Map<string, Map<string, Doubt>> = new Map(); // sessionId -> doubtId -> Doubt
private reactions: Map<string, Map<ReactionType, number>> = new Map(); // sessionId -> emoji -> count
private whiteboard: Map<string, WhiteboardStroke[]> = new Map(); // sessionId -> strokes
private currentSlide: Map<string, number> = new Map(); // sessionId -> slideNumber
// SSE clients for broadcasting
private sseClients: Map<string, Set<{
userId: string;
write: (data: string) => void;
}>> = new Map(); // sessionId -> clients
// Event ID counter for SSE
private eventIdCounter: number = 0;
// ═══════════════════════════════════════════════════════════════════════════
// SESSION MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
async createSession(
teacherId: string,
teacherName: string,
sessionName: string,
slideUrls: string[] = []
): Promise<Session> {
const sessionId = uuidv4();
const session: Session = {
id: sessionId,
name: sessionName,
teacherId,
teacherName,
createdAt: new Date().toISOString(),
isActive: true,
currentSlide: 0,
totalSlides: slideUrls.length,
slideUrls,
whiteboardData: [],
studentCount: 0,
maxStudents: 100000 // 1 Lakh
};
this.sessions.set(sessionId, session);
this.sessionUsers.set(sessionId, new Set([teacherId]));
this.polls.set(sessionId, new Map());
this.doubts.set(sessionId, new Map());
this.reactions.set(sessionId, new Map());
this.whiteboard.set(sessionId, []);
this.currentSlide.set(sessionId, 0);
this.sseClients.set(sessionId, new Set());
// Initialize reactions
const reactionEmojis: ReactionType[] = ['πŸ‘', '❀️', 'πŸŽ‰', 'πŸ˜•', 'πŸš€', 'πŸ‘', 'πŸ”₯'];
const sessionReactions = this.reactions.get(sessionId)!;
reactionEmojis.forEach(emoji => sessionReactions.set(emoji, 0));
// Save to storage
await storageService.saveSessionData(sessionId, session);
return session;
}
getSession(sessionId: string): Session | undefined {
return this.sessions.get(sessionId);
}
async endSession(sessionId: string): Promise<void> {
const session = this.sessions.get(sessionId);
if (session) {
session.isActive = false;
await this.broadcast(sessionId, 'session_ended', { sessionId });
// Export session data
await storageService.exportSession(sessionId);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// USER MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
joinSession(
sessionId: string,
userId: string,
userName: string,
role: 'teacher' | 'student'
): User {
const user: User = {
id: userId,
name: userName,
role,
sessionId,
joinedAt: new Date().toISOString()
};
this.users.set(userId, user);
const sessionUsers = this.sessionUsers.get(sessionId);
if (sessionUsers) {
sessionUsers.add(userId);
}
const session = this.sessions.get(sessionId);
if (session && role === 'student') {
session.studentCount++;
}
// Broadcast to all
this.broadcast(sessionId, 'student_joined', {
userId,
userName,
role,
studentCount: session?.studentCount || 0
});
return user;
}
leaveSession(userId: string): void {
const user = this.users.get(userId);
if (!user || !user.sessionId) return;
const sessionUsers = this.sessionUsers.get(user.sessionId);
if (sessionUsers) {
sessionUsers.delete(userId);
}
const session = this.sessions.get(user.sessionId);
if (session && user.role === 'student') {
session.studentCount--;
}
this.users.delete(userId);
// Broadcast to all
if (user.sessionId) {
this.broadcast(user.sessionId, 'student_left', {
userId,
userName: user.name,
studentCount: session?.studentCount || 0
});
}
}
getOnlineUsers(sessionId: string): PresenceUser[] {
const sessionUsers = this.sessionUsers.get(sessionId);
if (!sessionUsers) return [];
return Array.from(sessionUsers)
.map(userId => this.users.get(userId))
.filter(Boolean)
.map(user => ({
id: user!.id,
name: user!.name,
role: user!.role,
lastSeen: new Date().toISOString(),
isOnline: true
}));
}
// ═══════════════════════════════════════════════════════════════════════════
// SSE CLIENT MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
addSSEClient(sessionId: string, userId: string, write: (data: string) => void): void {
const clients = this.sseClients.get(sessionId);
if (clients) {
clients.add({ userId, write });
}
}
removeSSEClient(sessionId: string, userId: string): void {
const clients = this.sseClients.get(sessionId);
if (clients) {
const clientToRemove = Array.from(clients).find(c => c.userId === userId);
if (clientToRemove) {
clients.delete(clientToRemove);
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// BROADCASTING (SSE)
// ═══════════════════════════════════════════════════════════════════════════
async broadcast(sessionId: string, event: string, data: any): Promise<string> {
const eventId = `${++this.eventIdCounter}`;
const timestamp = new Date().toISOString();
const clients = this.sseClients.get(sessionId);
if (!clients) return eventId;
const message = `id: ${eventId}\nevent: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
Array.from(clients).forEach(client => {
try {
client.write(message);
} catch (error) {
console.error(`Failed to send to client ${client.userId}:`, error);
}
});
return eventId;
}
// ═══════════════════════════════════════════════════════════════════════════
// SLIDE MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
async changeSlide(sessionId: string, slideNumber: number): Promise<void> {
const session = this.sessions.get(sessionId);
if (!session) return;
session.currentSlide = slideNumber;
this.currentSlide.set(sessionId, slideNumber);
await this.broadcast(sessionId, 'slide_change', {
slideNumber,
slideUrl: session.slideUrls[slideNumber] || null,
totalSlides: session.totalSlides
});
}
getSlide(sessionId: string): number {
return this.currentSlide.get(sessionId) || 0;
}
// ═══════════════════════════════════════════════════════════════════════════
// WHITEBOARD MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
async addWhiteboardStroke(sessionId: string, stroke: WhiteboardStroke): Promise<void> {
const strokes = this.whiteboard.get(sessionId);
if (strokes) {
strokes.push(stroke);
}
await this.broadcast(sessionId, 'whiteboard_draw', stroke);
}
async clearWhiteboard(sessionId: string): Promise<void> {
this.whiteboard.set(sessionId, []);
await this.broadcast(sessionId, 'whiteboard_clear', { sessionId });
}
getWhiteboardStrokes(sessionId: string): WhiteboardStroke[] {
return this.whiteboard.get(sessionId) || [];
}
// ═══════════════════════════════════════════════════════════════════════════
// POLL MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
async createPoll(
sessionId: string,
question: string,
options: string[],
correctAnswer?: string,
duration: number = 60
): Promise<Poll> {
const pollId = uuidv4();
const now = new Date();
const endsAt = new Date(now.getTime() + duration * 1000).toISOString();
const poll: Poll = {
id: pollId,
sessionId,
question,
options: options.map((text, index) => ({
id: `option_${index}`,
text,
isCorrect: text === correctAnswer
})),
correctAnswer,
duration,
createdAt: now.toISOString(),
endsAt,
isActive: true,
results: {},
totalVotes: 0,
votedStudents: []
};
// Initialize results
options.forEach(opt => {
poll.results[opt] = 0;
});
const sessionPolls = this.polls.get(sessionId);
if (sessionPolls) {
sessionPolls.set(pollId, poll);
}
// Save to storage
await storageService.savePoll(sessionId, poll);
// Broadcast new poll
await this.broadcast(sessionId, 'new_poll', poll);
// Auto-end poll after duration
setTimeout(() => {
this.endPoll(sessionId, pollId);
}, duration * 1000);
return poll;
}
async votePoll(sessionId: string, pollId: string, option: string, studentId: string): Promise<Poll | null> {
const sessionPolls = this.polls.get(sessionId);
if (!sessionPolls) return null;
const poll = sessionPolls.get(pollId);
if (!poll || !poll.isActive) return null;
// Check if already voted
if (poll.votedStudents.includes(studentId)) {
return poll;
}
// Record vote
poll.results[option] = (poll.results[option] || 0) + 1;
poll.totalVotes++;
poll.votedStudents.push(studentId);
// Save to storage
await storageService.savePoll(sessionId, poll);
// Broadcast update
await this.broadcast(sessionId, 'poll_update', {
pollId,
results: poll.results,
totalVotes: poll.totalVotes
});
return poll;
}
async endPoll(sessionId: string, pollId: string): Promise<void> {
const sessionPolls = this.polls.get(sessionId);
if (!sessionPolls) return;
const poll = sessionPolls.get(pollId);
if (!poll || !poll.isActive) return;
poll.isActive = false;
// Save to storage
await storageService.savePoll(sessionId, poll);
// Broadcast results
await this.broadcast(sessionId, 'poll_ended', {
pollId,
results: poll.results,
totalVotes: poll.totalVotes,
correctAnswer: poll.correctAnswer
});
}
getActivePoll(sessionId: string): Poll | null {
const sessionPolls = this.polls.get(sessionId);
if (!sessionPolls) return null;
let activePoll: Poll | null = null;
sessionPolls.forEach(poll => {
if (poll.isActive && !activePoll) {
activePoll = poll;
}
});
return activePoll;
}
// ═══════════════════════════════════════════════════════════════════════════
// CHAT MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
async sendChatMessage(
sessionId: string,
userId: string,
userName: string,
message: string,
isTeacher: boolean
): Promise<ChatMessage> {
const chatMessage: ChatMessage = {
id: uuidv4(),
sessionId,
userId,
userName,
message,
isTeacher,
timestamp: new Date().toISOString()
};
// Save to storage
await storageService.saveChatMessage(sessionId, chatMessage);
// Broadcast to all
await this.broadcast(sessionId, 'chat_message', chatMessage);
return chatMessage;
}
// ═══════════════════════════════════════════════════════════════════════════
// DOUBT MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
async createDoubt(
sessionId: string,
studentId: string,
studentName: string,
question: string,
image?: string,
slideNumber?: number
): Promise<Doubt> {
const doubtId = uuidv4();
const doubt: Doubt = {
id: doubtId,
sessionId,
studentId,
studentName,
question,
image,
slideNumber: slideNumber ?? this.currentSlide.get(sessionId) ?? 0,
status: 'pending',
createdAt: new Date().toISOString()
};
const sessionDoubts = this.doubts.get(sessionId);
if (sessionDoubts) {
sessionDoubts.set(doubtId, doubt);
}
// Save to storage
await storageService.saveDoubt(sessionId, doubt);
// Broadcast to teacher
await this.broadcast(sessionId, 'new_doubt', doubt);
return doubt;
}
async solveDoubt(
sessionId: string,
doubtId: string,
annotatedImage?: string,
teacherResponse?: string
): Promise<Doubt | null> {
const sessionDoubts = this.doubts.get(sessionId);
if (!sessionDoubts) return null;
const doubt = sessionDoubts.get(doubtId);
if (!doubt) return null;
doubt.status = 'solved';
doubt.solvedAt = new Date().toISOString();
doubt.annotatedImage = annotatedImage;
doubt.teacherResponse = teacherResponse;
// Save to storage
await storageService.saveDoubt(sessionId, doubt);
// Broadcast solution
await this.broadcast(sessionId, 'doubt_solved', doubt);
return doubt;
}
getDoubts(sessionId: string): Doubt[] {
const sessionDoubts = this.doubts.get(sessionId);
if (!sessionDoubts) return [];
return Array.from(sessionDoubts.values());
}
// ═══════════════════════════════════════════════════════════════════════════
// REACTIONS MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
async addReaction(sessionId: string, emoji: ReactionType): Promise<Record<ReactionType, number>> {
const sessionReactions = this.reactions.get(sessionId);
if (!sessionReactions) return {} as Record<ReactionType, number>;
const currentCount = sessionReactions.get(emoji) || 0;
sessionReactions.set(emoji, currentCount + 1);
const allReactions = {} as Record<ReactionType, number>;
sessionReactions.forEach((count, em) => {
allReactions[em as ReactionType] = count;
});
// Broadcast update
await this.broadcast(sessionId, 'reaction', {
reactions: allReactions,
lastReaction: emoji
});
return allReactions;
}
getReactions(sessionId: string): Record<ReactionType, number> {
const sessionReactions = this.reactions.get(sessionId);
if (!sessionReactions) return {} as Record<ReactionType, number>;
const allReactions = {} as Record<ReactionType, number>;
sessionReactions.forEach((count, emoji) => {
allReactions[emoji as ReactionType] = count;
});
return allReactions;
}
// ═══════════════════════════════════════════════════════════════════════════
// PRESENCE UPDATE
// ═══════════════════════════════════════════════════════════════════════════
async sendPresenceUpdate(sessionId: string): Promise<void> {
const users = this.getOnlineUsers(sessionId);
const session = this.sessions.get(sessionId);
await this.broadcast(sessionId, 'presence_update', {
sessionId,
onlineCount: users.length,
studentCount: session?.studentCount || 0,
users: users.slice(0, 100) // Send only first 100 for bandwidth
});
}
}
// Singleton instance
export const sessionManager = new SessionManager();