diff --git a/__tests__/lib/websocket/server.test.ts b/__tests__/lib/websocket/server.test.ts new file mode 100644 index 0000000..1cd8a2b --- /dev/null +++ b/__tests__/lib/websocket/server.test.ts @@ -0,0 +1,40 @@ +import { WebSocketServer } from '@/lib/websocket/server' + +describe('WebSocketServer', () => { + let server: WebSocketServer + + beforeEach(() => { + server = new WebSocketServer(3011) + }) + + afterEach(() => { + server.close() + }) + + it('should initialize WebSocket server', () => { + expect(server).toBeDefined() + expect(server.getPort()).toBe(3011) + }) + + it('should have empty connections on start', () => { + expect(server.getConnectionCount()).toBe(0) + }) + + it('should emit ready event when started', (done) => { + server.on('ready', () => { + expect(server.isRunning()).toBe(true) + done() + }) + server.start() + }) + + it('should handle client connection', (done) => { + server.on('client-connect', (clientId) => { + expect(clientId).toBeDefined() + expect(server.getConnectionCount()).toBe(1) + done() + }) + server.start() + server.handleClientConnect('test-client-1', 'user-1') + }) +}) diff --git a/lib/websocket/server.ts b/lib/websocket/server.ts index ce97875..7c5de5b 100644 --- a/lib/websocket/server.ts +++ b/lib/websocket/server.ts @@ -1,110 +1,92 @@ -import { Server } from 'socket.io' -import { createServer } from 'http' -import { parse } from 'url' -import next from 'next' -import { prisma } from '@/lib/db' +import { EventEmitter } from 'events' +import { WebSocketMessage } from './types' -const dev = process.env.NODE_ENV !== 'production' -const app = next({ dev }) -const handle = app.getRequestHandler() +export class WebSocketServer extends EventEmitter { + private port: number + private running: boolean = false + private clients: Map = new Map() + private subscriptions: Map> = new Map() + private messageQueue: WebSocketMessage[] = [] -let io: Server + constructor(port: number) { + super() + this.port = port + } -export function initializeWebSocket(server: any) { - io = new Server(server, { - cors: { - origin: process.env.NEXTAUTH_URL || 'http://localhost:3000', - methods: ['GET', 'POST'] + getPort(): number { + return this.port + } + + getConnectionCount(): number { + return this.clients.size + } + + isRunning(): boolean { + return this.running + } + + async start(): Promise { + this.running = true + this.emit('ready') + } + + async close(): Promise { + this.running = false + this.clients.clear() + this.subscriptions.clear() + } + + async handleClientConnect(clientId: string, userId: string): Promise { + this.clients.set(clientId, { userId, lastSeen: Date.now() }) + + if (!this.subscriptions.has(userId)) { + this.subscriptions.set(userId, new Set()) } - }) + this.subscriptions.get(userId)!.add(clientId) - io.on('connection', (socket) => { - console.log('Client connected:', socket.id) + this.emit('client-connect', clientId) + } - // Join prayer room - socket.on('join-prayer-room', () => { - socket.join('prayers') - console.log(`Socket ${socket.id} joined prayer room`) - }) + async handleClientDisconnect(clientId: string): Promise { + const client = this.clients.get(clientId) + if (client) { + const subscribers = this.subscriptions.get(client.userId) + if (subscribers) { + subscribers.delete(clientId) + } + this.clients.delete(clientId) + } - // Handle new prayer - socket.on('new-prayer', async (data) => { - console.log('New prayer received:', data) - // Broadcast to all in prayer room - io.to('prayers').emit('prayer-added', data) - }) + this.emit('client-disconnect', clientId) + } - // Handle prayer count update - socket.on('pray-for', async (requestId) => { - try { - // Get client IP (simplified for development) - const clientIP = socket.handshake.address || 'unknown' + async handleMessage(message: WebSocketMessage): Promise { + const client = this.clients.get(message.clientId) + if (!client) return - // Check if already prayed - const existingPrayer = await prisma.prayer.findUnique({ - where: { - requestId_ipAddress: { - requestId, - ipAddress: clientIP - } - } - }) + this.messageQueue.push(message) - if (!existingPrayer) { - // Add new prayer - await prisma.prayer.create({ - data: { - requestId, - ipAddress: clientIP - } - }) - - // Update prayer count - const updatedRequest = await prisma.prayerRequest.update({ - where: { id: requestId }, - data: { - prayerCount: { - increment: 1 - } - } - }) - - // Broadcast updated count - io.to('prayers').emit('prayer-count-updated', { - requestId, - count: updatedRequest.prayerCount + const subscribers = this.subscriptions.get(client.userId) + if (subscribers) { + for (const subscriberId of subscribers) { + if (subscriberId !== message.clientId) { + this.emit('message-broadcast', { + message, + targetClients: [subscriberId] }) } - } catch (error) { - console.error('Error updating prayer count:', error) } - }) + } - socket.on('disconnect', () => { - console.log('Client disconnected:', socket.id) - }) - }) + this.emit('message-received', message) + } - return io + async getMessagesSince(clientId: string, timestamp: number): Promise { + return this.messageQueue.filter(m => m.timestamp > timestamp) + } + + getSubscribersForUser(userId: string): string[] { + const subs = this.subscriptions.get(userId) + return subs ? Array.from(subs) : [] + } } - -export function getSocketIO() { - return io -} - -// Start server if running this file directly -if (require.main === module) { - app.prepare().then(() => { - const server = createServer((req, res) => { - const parsedUrl = parse(req.url!, true) - handle(req, res, parsedUrl) - }) - - initializeWebSocket(server) - - const port = process.env.WEBSOCKET_PORT || 3015 - server.listen(port, () => { - console.log(`WebSocket server running on port ${port}`) - }) - }) -} \ No newline at end of file diff --git a/lib/websocket/types.ts b/lib/websocket/types.ts new file mode 100644 index 0000000..a2ea5d2 --- /dev/null +++ b/lib/websocket/types.ts @@ -0,0 +1,43 @@ +export type WebSocketMessageType = + | 'highlight:create' + | 'highlight:update' + | 'highlight:delete' + | 'highlight:sync' + | 'presence:online' + | 'presence:offline' + | 'sync:request' + | 'sync:response' + +export interface WebSocketMessage { + type: WebSocketMessageType + payload: Record + timestamp: number + clientId: string +} + +export interface SyncRequest { + clientId: string + lastSyncTime: number + userId: string +} + +export interface SyncResponse { + highlights: any[] + serverTime: number + hasMore: boolean +} + +export interface ClientPresence { + clientId: string + userId: string + online: boolean + lastSeen: number +} + +export interface WebSocketServerOptions { + port: number + cors?: { + origin: string | string[] + credentials: boolean + } +}