atozas-push-notification
Version:
Real-time push notifications across platforms using socket.io
416 lines (346 loc) • 11.2 kB
text/typescript
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}`);
}
}