realtimecursor
Version:
Real-time collaboration system with cursor tracking and approval workflow
305 lines (255 loc) ⢠8.49 kB
JavaScript
const express = require('express');
const cors = require('cors');
const http = require('http');
const { Server } = require('socket.io');
// Create a simple analytics service
const AnalyticsService = {
recordProjectJoin: (projectId, userId) => {
console.log(`Analytics: User ${userId} joined project ${projectId}`);
},
recordProjectLeave: (projectId, userId) => {
console.log(`Analytics: User ${userId} left project ${projectId}`);
},
trackConnection: (socketId, key, projectId, userId) => {
console.log(`Analytics: Connection tracked for ${socketId}`);
},
trackDisconnection: (socketId) => {
console.log(`Analytics: Disconnection tracked for ${socketId}`);
},
trackContentUpdate: (projectId, userId, length) => {
console.log(`Analytics: Content updated in project ${projectId} by user ${userId}`);
}
};
// Create a simple API key service
const ApiKeyService = {
validateApiKey: (apiKey) => {
return { valid: true, userId: 'user-123' };
}
};
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: '*',
methods: ['GET', 'POST'],
credentials: true
}
});
// Track connected users by project
const projectUsers = {};
// Track socket to user mapping
const socketToUser = {};
const PORT = process.env.PORT || 3001;
app.use(cors({
origin: '*',
credentials: true
}));
app.use(express.json());
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// Root endpoint
app.get('/', (req, res) => {
res.json({ status: 'OK', message: 'RealtimeCursor API is running' });
});
// Real-time collaboration rooms
const rooms = new Map();
// Socket.IO connection handling
io.on('connection', (socket) => {
console.log('New client connected:', socket.id);
// Store user data to prevent duplicate join events
const userJoinedProjects = new Map();
// Handle project join
socket.on('join-project', (data) => {
const { projectId, user } = data;
if (!projectId || !user || !user.id) {
socket.emit('error', { message: 'Invalid join data' });
return;
}
// Prevent duplicate join events for the same project
const existingProject = userJoinedProjects.get(projectId);
if (existingProject) {
return; // User already joined this project
}
// Join the room
socket.join(projectId);
socket.projectId = projectId;
socket.user = user;
// Mark this project as joined
userJoinedProjects.set(projectId, true);
// Initialize project users if needed
if (!projectUsers[projectId]) {
projectUsers[projectId] = {};
}
// Store user data
projectUsers[projectId][user.id] = {
...user,
socketId: socket.id,
lastActivity: Date.now()
};
// Store socket to user mapping
socketToUser[socket.id] = {
userId: user.id,
projectId
};
// Initialize room if needed
if (!rooms.has(projectId)) {
rooms.set(projectId, new Map());
}
const room = rooms.get(projectId);
room.set(socket.id, { ...user, socketId: socket.id });
// Send current users to the new user
const usersInProject = Object.values(projectUsers[projectId]);
socket.emit('room-users', { users: usersInProject });
// Notify others about the new user
socket.to(projectId).emit('user-joined', { user: { ...user, socketId: socket.id } });
console.log(`User ${user.id} joined project ${projectId}`);
AnalyticsService.recordProjectJoin(projectId, user.id);
});
// Handle cursor position updates
socket.on('cursor-position', (data) => {
const { projectId, position } = data;
const socketData = socketToUser[socket.id];
if (!socketData || !projectId || socketData.projectId !== projectId) {
return;
}
const userId = socketData.userId;
// Update user's cursor position
if (projectUsers[projectId] && projectUsers[projectId][userId]) {
projectUsers[projectId][userId].position = position;
projectUsers[projectId][userId].lastActivity = Date.now();
// Broadcast to others in the project
socket.to(projectId).emit('cursor-update', {
userId,
position
});
}
});
// Handle cursor move
socket.on('cursor-move', ({ x, y, relativeX, relativeY, textPosition }) => {
if (socket.projectId) {
socket.to(socket.projectId).emit('cursor-update', {
x, y, relativeX, relativeY, textPosition,
user: socket.user, socketId: socket.id,
timestamp: Date.now()
});
}
});
// Handle content updates
socket.on('content-update', (data) => {
const { projectId, content, version } = data;
const socketData = socketToUser[socket.id];
if (!socketData || !projectId || socketData.projectId !== projectId) {
return;
}
// Broadcast to others in the project
socket.to(projectId).emit('content-update', {
userId: socketData.userId,
content,
version
});
});
// Handle content change
socket.on('content-change', ({ content, cursorPosition }) => {
if (socket.projectId) {
// Track content update for analytics
AnalyticsService.trackContentUpdate(
socket.projectId,
socket.user?.id || 'anonymous',
content ? content.length : 0
);
socket.to(socket.projectId).emit('content-update', {
content,
cursorPosition,
user: socket.user,
socketId: socket.id
});
}
});
// Handle typing status
socket.on('user-typing', ({ isTyping }) => {
if (socket.projectId && socket.user) {
socket.to(socket.projectId).emit('user-typing', {
socketId: socket.id,
isTyping,
user: socket.user
});
}
});
// Handle disconnection
socket.on('disconnect', () => {
const socketData = socketToUser[socket.id];
if (socketData) {
const { projectId, userId } = socketData;
if (projectUsers[projectId] && projectUsers[projectId][userId]) {
// Remove user from project
const userData = projectUsers[projectId][userId];
delete projectUsers[projectId][userId];
// Notify others about the user leaving
socket.to(projectId).emit('user-left', { userId });
console.log(`User ${userId} left project ${projectId}`);
AnalyticsService.recordProjectLeave(projectId, userId);
}
// Clean up empty projects
if (projectUsers[projectId] && Object.keys(projectUsers[projectId]).length === 0) {
delete projectUsers[projectId];
}
// Remove socket mapping
delete socketToUser[socket.id];
}
// Clean up room data
if (socket.projectId) {
const room = rooms.get(socket.projectId);
if (room) {
room.delete(socket.id);
if (room.size === 0) {
rooms.delete(socket.projectId);
} else {
socket.to(socket.projectId).emit('user-left', { socketId: socket.id });
}
}
}
console.log('Client disconnected:', socket.id);
AnalyticsService.trackDisconnection(socket.id);
});
});
// Start server with error handling
server.listen(PORT, () => {
console.log(`š RealtimeCursor API server running on port ${PORT}`);
console.log(`ā
Server is ready to accept connections!`);
});
server.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
console.error(`ā Port ${PORT} is already in use. Please use a different port or kill the existing process.`);
process.exit(1);
} else {
console.error('ā Server error:', error);
}
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('\nš SIGTERM received, shutting down gracefully...');
server.close(() => {
console.log('ā
Server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('\nš SIGINT received, shutting down gracefully...');
server.close(() => {
console.log('ā
Server closed');
process.exit(0);
});
});
// 404 handler - must be the last middleware
app.use((req, res) => {
console.log(`404 Not Found: ${req.method} ${req.originalUrl}`);
res.status(404).json({
success: false,
message: 'Endpoint not found',
path: req.originalUrl,
method: req.method
});
});
module.exports = app;