UNPKG

atozas-push-notification

Version:

Real-time push notifications across platforms using socket.io

416 lines (346 loc) 11.2 kB
import { Server, Socket } from 'socket.io'; import { createServer } from 'http'; import { ServerConfig, UserInfo, NotificationData, NotificationOptions, SendNotificationParams } from './types'; interface ConnectedUser { socket: Socket; userInfo: UserInfo; groups: Set<string>; lastSeen: number; } export class AtozasPushNotificationServer { private io: Server; private httpServer: any; private connectedUsers: Map<string, ConnectedUser> = new Map(); private userSocketMap: Map<string, string> = new Map(); // userId -> socketId private groups: Map<string, Set<string>> = new Map(); // groupId -> Set of userIds constructor(config: ServerConfig = {}) { this.httpServer = createServer(); this.io = new Server(this.httpServer, { cors: config.cors || { origin: "*", methods: ["GET", "POST"] }, allowEIO3: config.allowEIO3 || true, transports: config.transports as any || ['websocket', 'polling'] }); this.setupEventHandlers(); if (config.port) { this.listen(config.port); } } /** * Start the server on specified port */ public listen(port: number): void { this.httpServer.listen(port, () => { console.log(`Atozas Push Notification Server running on port ${port}`); }); } /** * Send notification to specific user(s), group(s), or broadcast to all */ public sendNotification(params: SendNotificationParams): boolean { const { to, target, notification, options } = params; // Add timestamp if not provided if (!notification.timestamp) { notification.timestamp = Date.now(); } const payload = { notification, options }; switch (to) { case 'user': return this.sendToUser(target as string, payload); case 'group': return this.sendToGroup(target as string, payload); case 'broadcast': return this.sendBroadcast(payload); default: console.error('Invalid "to" parameter. Must be "user", "group", or "broadcast"'); return false; } } /** * Send notification to multiple users */ public sendToUsers(userIds: string[], notification: NotificationData, options?: NotificationOptions): boolean { const payload = { notification: { ...notification, timestamp: notification.timestamp || Date.now() }, options }; let success = true; userIds.forEach(userId => { if (!this.sendToUser(userId, payload)) { success = false; } }); return success; } /** * Send notification to multiple groups */ public sendToGroups(groupIds: string[], notification: NotificationData, options?: NotificationOptions): boolean { const payload = { notification: { ...notification, timestamp: notification.timestamp || Date.now() }, options }; let success = true; groupIds.forEach(groupId => { if (!this.sendToGroup(groupId, payload)) { success = false; } }); return success; } /** * Get list of connected users */ public getConnectedUsers(): UserInfo[] { return Array.from(this.connectedUsers.values()).map(user => user.userInfo); } /** * Get user connection status */ public isUserOnline(userId: string): boolean { return this.userSocketMap.has(userId); } /** * Get users in a specific group */ public getGroupUsers(groupId: string): string[] { const group = this.groups.get(groupId); return group ? Array.from(group) : []; } /** * Get all groups */ public getAllGroups(): string[] { return Array.from(this.groups.keys()); } /** * Add user to group programmatically */ public addUserToGroup(userId: string, groupId: string): boolean { if (!this.groups.has(groupId)) { this.groups.set(groupId, new Set()); } this.groups.get(groupId)!.add(userId); // Update user's groups if connected const socketId = this.userSocketMap.get(userId); if (socketId) { const user = this.connectedUsers.get(socketId); if (user) { user.groups.add(groupId); user.socket.join(groupId); } } return true; } /** * Remove user from group programmatically */ public removeUserFromGroup(userId: string, groupId: string): boolean { const group = this.groups.get(groupId); if (!group) return false; group.delete(userId); // Clean up empty groups if (group.size === 0) { this.groups.delete(groupId); } // Update user's groups if connected const socketId = this.userSocketMap.get(userId); if (socketId) { const user = this.connectedUsers.get(socketId); if (user) { user.groups.delete(groupId); user.socket.leave(groupId); } } return true; } /** * Get server statistics */ public getStats(): any { return { connectedUsers: this.connectedUsers.size, totalGroups: this.groups.size, groupStats: Array.from(this.groups.entries()).map(([groupId, users]) => ({ groupId, userCount: users.size })) }; } /** * Disconnect user */ public disconnectUser(userId: string, reason?: string): boolean { const socketId = this.userSocketMap.get(userId); if (!socketId) return false; const user = this.connectedUsers.get(socketId); if (user) { user.socket.disconnect(true); return true; } return false; } /** * Send notification to a specific user */ private sendToUser(userId: string, payload: any): boolean { const socketId = this.userSocketMap.get(userId); if (!socketId) { console.warn(`User ${userId} is not connected`); return false; } const user = this.connectedUsers.get(socketId); if (!user) { console.warn(`User ${userId} connection not found`); return false; } user.socket.emit('notification', payload); return true; } /** * Send notification to a group */ private sendToGroup(groupId: string, payload: any): boolean { const group = this.groups.get(groupId); if (!group || group.size === 0) { console.warn(`Group ${groupId} not found or empty`); return false; } this.io.to(groupId).emit('notification', payload); return true; } /** * Broadcast notification to all connected users */ private sendBroadcast(payload: any): boolean { this.io.emit('notification', payload); return true; } /** * Setup socket event handlers */ private setupEventHandlers(): void { this.io.on('connection', (socket: Socket) => { console.log(`Client connected: ${socket.id}`); // Handle user authentication socket.on('authenticate', (userInfo: UserInfo) => { this.handleUserAuthentication(socket, userInfo); }); // Handle joining groups socket.on('join_group', (groupId: string) => { this.handleJoinGroup(socket, groupId); }); // Handle leaving groups socket.on('leave_group', (groupId: string) => { this.handleLeaveGroup(socket, groupId); }); // Handle notification acknowledgment socket.on('notification_ack', (notificationId: string) => { console.log(`Notification ${notificationId} acknowledged by ${socket.id}`); }); // Handle user status requests socket.on('request_user_status', (userId: string) => { const isOnline = this.isUserOnline(userId); socket.emit('user_status', { userId, online: isOnline }); }); // Handle disconnection socket.on('disconnect', (reason) => { this.handleUserDisconnection(socket, reason); }); // Handle errors socket.on('error', (error) => { console.error(`Socket error for ${socket.id}:`, error); }); }); } /** * Handle user authentication */ private handleUserAuthentication(socket: Socket, userInfo: UserInfo): void { // Remove existing connection for this user if exists const existingSocketId = this.userSocketMap.get(userInfo.userId); if (existingSocketId && existingSocketId !== socket.id) { const existingUser = this.connectedUsers.get(existingSocketId); if (existingUser) { existingUser.socket.disconnect(true); this.connectedUsers.delete(existingSocketId); } } // Add new connection this.connectedUsers.set(socket.id, { socket, userInfo, groups: new Set(), lastSeen: Date.now() }); this.userSocketMap.set(userInfo.userId, socket.id); console.log(`User authenticated: ${userInfo.userId} (${socket.id})`); // Rejoin groups if user was in any const userGroups = Array.from(this.groups.entries()) .filter(([_, users]) => users.has(userInfo.userId)) .map(([groupId]) => groupId); userGroups.forEach(groupId => { socket.join(groupId); this.connectedUsers.get(socket.id)!.groups.add(groupId); }); } /** * Handle joining a group */ private handleJoinGroup(socket: Socket, groupId: string): void { const user = this.connectedUsers.get(socket.id); if (!user) { console.warn(`User not authenticated for socket ${socket.id}`); return; } // Add to group if (!this.groups.has(groupId)) { this.groups.set(groupId, new Set()); } this.groups.get(groupId)!.add(user.userInfo.userId); user.groups.add(groupId); socket.join(groupId); console.log(`User ${user.userInfo.userId} joined group ${groupId}`); } /** * Handle leaving a group */ private handleLeaveGroup(socket: Socket, groupId: string): void { const user = this.connectedUsers.get(socket.id); if (!user) return; const group = this.groups.get(groupId); if (group) { group.delete(user.userInfo.userId); // Clean up empty groups if (group.size === 0) { this.groups.delete(groupId); } } user.groups.delete(groupId); socket.leave(groupId); console.log(`User ${user.userInfo.userId} left group ${groupId}`); } /** * Handle user disconnection */ private handleUserDisconnection(socket: Socket, reason: string): void { const user = this.connectedUsers.get(socket.id); if (user) { console.log(`User ${user.userInfo.userId} disconnected: ${reason}`); // Remove from user mapping this.userSocketMap.delete(user.userInfo.userId); // Note: We don't remove from groups on disconnect to allow rejoining // Groups are only cleaned up when explicitly leaving or on server restart } this.connectedUsers.delete(socket.id); console.log(`Client disconnected: ${socket.id}`); } }