Files
William Valentin bb9c8ec1c3 feat: Migrate from Socket.IO to Server-Sent Events (SSE)
- Replace Socket.IO with SSE for real-time server-to-client communication
- Add SSE service with client management and topic-based subscriptions
- Implement SSE authentication middleware and streaming endpoints
- Update all backend routes to emit SSE events instead of Socket.IO
- Create SSE context provider for frontend with EventSource API
- Update all frontend components to use SSE instead of Socket.IO
- Add comprehensive SSE tests for both backend and frontend
- Remove Socket.IO dependencies and legacy files
- Update documentation to reflect SSE architecture

Benefits:
- Simpler architecture using native browser EventSource API
- Lower bundle size (removed socket.io-client dependency)
- Better compatibility with reverse proxies and load balancers
- Reduced resource usage for Raspberry Pi deployment
- Standard HTTP-based real-time communication

🤖 Generated with [AI Assistant]

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
2025-12-05 22:49:22 -08:00

360 lines
8.8 KiB
JavaScript

const express = require("express");
const Event = require("../models/Event");
const User = require("../models/User");
const auth = require("../middleware/auth");
const { asyncHandler } = require("../middleware/errorHandler");
const {
createEventValidation,
eventIdValidation,
} = require("../middleware/validators/eventValidator");
const { paginate, buildPaginatedResponse } = require("../middleware/pagination");
const couchdbService = require("../services/couchdbService");
const { getCacheMiddleware, invalidateCacheByPattern } = require("../middleware/cache");
const router = express.Router();
// Get all events (with pagination)
router.get(
"/",
paginate,
getCacheMiddleware(120), // Cache for 2 minutes
asyncHandler(async (req, res) => {
const { page, limit } = req.pagination;
const result = await Event.getAllPaginated(page, limit);
// Transform participants data to match expected format
const events = result.events.map(event => ({
...event,
participants: event.participants.map(p => ({
_id: p.userId,
name: p.name,
profilePicture: p.profilePicture
}))
}));
res.json(buildPaginatedResponse(events, result.pagination.totalCount, page, limit));
}),
);
// Create an event
router.post(
"/",
auth,
createEventValidation,
asyncHandler(async (req, res) => {
const { title, description, date, location } = req.body;
const event = await Event.create({
title,
description,
date,
location,
});
// Invalidate events cache
invalidateCacheByPattern('/api/events');
// Emit SSE event for new event
const sse = req.app.get("sse");
if (sse) {
sse.broadcastToTopic("events", "eventUpdate", {
type: "new_event",
event,
});
}
res.json(event);
}),
);
// RSVP to an event
router.put(
"/rsvp/:id",
auth,
eventIdValidation,
asyncHandler(async (req, res) => {
const eventId = req.params.id;
const userId = req.user.id;
// Check if event exists
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ msg: "Event not found" });
}
// Check if user has already RSVPed
const alreadyParticipating = event.participants.some(p => p.userId === userId);
if (alreadyParticipating) {
return res.status(400).json({ msg: "Already RSVPed" });
}
// Get user data for embedding
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ msg: "User not found" });
}
// Add participant to event
const updatedEvent = await Event.addParticipant(
eventId,
userId,
user.name,
user.profilePicture
);
// Update user's events array
if (!user.events.includes(eventId)) {
user.events.push(eventId);
user.stats.eventsParticipated = user.events.length;
if (typeof user.save === 'function') {
await user.save();
} else if (typeof User.update === 'function') {
await User.update(userId, { events: user.events, stats: user.stats });
}
}
// Award points for event participation using couchdbService
const updatedUser = await couchdbService.updateUserPoints(
userId,
15,
`Joined event: ${event.title}`,
{
entityType: 'Event',
entityId: eventId,
entityName: event.title
}
);
// Check and award badges
await couchdbService.checkAndAwardBadges(userId, updatedUser.points);
// Emit SSE event for RSVP
const sse = req.app.get("sse");
if (sse) {
sse.broadcastToTopic(`event_${eventId}`, "eventUpdate", {
type: "participants_updated",
eventId,
participants: updatedEvent.participants,
});
}
res.json({
participants: updatedEvent.participants,
pointsAwarded: 15,
newBalance: updatedUser.points,
});
}),
);
// Get event by ID
router.get(
"/:id",
eventIdValidation,
asyncHandler(async (req, res) => {
const event = await Event.findById(req.params.id);
if (!event) {
return res.status(404).json({ msg: "Event not found" });
}
// Transform participants data to match expected format
const transformedEvent = {
...event,
participants: event.participants.map(p => ({
_id: p.userId,
name: p.name,
profilePicture: p.profilePicture
}))
};
res.json(transformedEvent);
})
);
// Update event
router.put(
"/:id",
auth,
eventIdValidation,
createEventValidation,
asyncHandler(async (req, res) => {
const { title, description, date, location, status } = req.body;
const event = await Event.findById(req.params.id);
if (!event) {
return res.status(404).json({ msg: "Event not found" });
}
const updateData = { title, description, date, location };
if (status) {
updateData.status = status;
}
const updatedEvent = await Event.update(req.params.id, updateData);
// Emit SSE event for event update
const sse = req.app.get("sse");
if (sse) {
sse.broadcastToTopic(`event_${req.params.id}`, "eventUpdate", {
type: "event_updated",
event: updatedEvent,
});
}
res.json(updatedEvent);
})
);
// Update event status
router.patch(
"/:id/status",
auth,
eventIdValidation,
asyncHandler(async (req, res) => {
const { status } = req.body;
if (!["upcoming", "ongoing", "completed", "cancelled"].includes(status)) {
return res.status(400).json({ msg: "Invalid status" });
}
const updatedEvent = await Event.updateStatus(req.params.id, status);
res.json(updatedEvent);
})
);
// Cancel RSVP
router.delete(
"/rsvp/:id",
auth,
eventIdValidation,
asyncHandler(async (req, res) => {
const eventId = req.params.id;
const userId = req.user.id;
// Check if event exists
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ msg: "Event not found" });
}
// Check if user is participating
const isParticipating = event.participants.some(p => p.userId === userId);
if (!isParticipating) {
return res.status(400).json({ msg: "Not participating in this event" });
}
// Remove participant from event
const updatedEvent = await Event.removeParticipant(eventId, userId);
// Update user's events array
const user = await User.findById(userId);
if (user) {
user.events = user.events.filter(id => id !== eventId);
user.stats.eventsParticipated = user.events.length;
await user.save();
}
res.json({
participants: updatedEvent.participants,
msg: "RSVP cancelled successfully"
});
})
);
// Delete event
router.delete(
"/:id",
auth,
eventIdValidation,
asyncHandler(async (req, res) => {
const event = await Event.findById(req.params.id);
if (!event) {
return res.status(404).json({ msg: "Event not found" });
}
await Event.delete(req.params.id);
// Emit SSE event for event deletion
const sse = req.app.get("sse");
if (sse) {
sse.broadcastToTopic(`event_${req.params.id}`, "eventUpdate", {
type: "event_deleted",
eventId: req.params.id,
});
}
res.json({ msg: "Event deleted successfully" });
})
);
// Get upcoming events
router.get(
"/upcoming/list",
asyncHandler(async (req, res) => {
const { limit = 10 } = req.query;
const events = await Event.getUpcomingEvents(parseInt(limit));
// Transform participants data
const transformedEvents = events.map(event => ({
...event,
participants: event.participants.map(p => ({
_id: p.userId,
name: p.name,
profilePicture: p.profilePicture
}))
}));
res.json(transformedEvents);
})
);
// Get events by status
router.get(
"/status/:status",
asyncHandler(async (req, res) => {
const { status } = req.params;
if (!["upcoming", "ongoing", "completed", "cancelled"].includes(status)) {
return res.status(400).json({ msg: "Invalid status" });
}
const events = await Event.findByStatus(status);
// Transform participants data
const transformedEvents = events.map(event => ({
...event,
participants: event.participants.map(p => ({
_id: p.userId,
name: p.name,
profilePicture: p.profilePicture
}))
}));
res.json(transformedEvents);
})
);
// Get user's events
router.get(
"/user/:userId",
auth,
asyncHandler(async (req, res) => {
const { userId } = req.params;
const events = await Event.getEventsByUser(userId);
// Transform participants data
const transformedEvents = events.map(event => ({
...event,
participants: event.participants.map(p => ({
_id: p.userId,
name: p.name,
profilePicture: p.profilePicture
}))
}));
res.json(transformedEvents);
})
);
module.exports = router;