/**
* Session Manager for managing WhatsApp client instances.
* Handles client pooling, session lifecycle, and authentication state.
*/
import { Whatsapp, create, SocketState } from '@wppconnect-team/wppconnect';
import { SessionStatus, Session } from '../types/index.js';
import { tokenStorage } from './tokenStorage.js';
import { qrStorage } from './qrStorage.js';
import { logger } from './logger.js';
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || '10');
const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours
class SessionManager {
private clientMap: Map<string, Whatsapp> = new Map();
private sessionMap: Map<string, Session> = new Map();
private qrListeners: Map<string, (qr: string) => void> = new Map();
/**
* Initialize or get an existing session.
*/
async initializeSession(sessionId: string): Promise<{ ok: boolean; status: SessionStatus; qr?: string; error?: string }> {
try {
logger.info('SessionManager.initializeSession', `Initializing session ${sessionId}`);
// Check if session already exists and is authenticated
if (this.clientMap.has(sessionId)) {
const client = this.clientMap.get(sessionId)!;
const state = await client.getConnectionState();
if (state === SocketState.CONNECTED) {
return { ok: true, status: SessionStatus.AUTHENTICATED };
}
}
// Check if max sessions reached
if (this.clientMap.size >= MAX_SESSIONS && !this.clientMap.has(sessionId)) {
return {
ok: false,
status: SessionStatus.ERROR,
error: `Maximum concurrent sessions (${MAX_SESSIONS}) reached`,
};
}
// Create new client
let qrCode: string | null = null;
const client = await create({
session: sessionId,
catchQR: (qr) => {
qrCode = qr;
this.handleQRGenerated(sessionId, qr);
},
logQR: false,
});
this.clientMap.set(sessionId, client);
// Setup event listeners
this.setupClientListeners(sessionId, client);
// Create session record
const session: Session = {
sessionId,
status: SessionStatus.WAITING_QR,
createdAt: new Date(),
lastActivity: new Date(),
};
this.sessionMap.set(sessionId, session);
logger.info('SessionManager.initializeSession', `Session ${sessionId} initialized`);
if (qrCode) {
return { ok: true, status: SessionStatus.WAITING_QR, qr: qrCode };
}
return { ok: true, status: SessionStatus.WAITING_QR };
} catch (error) {
logger.error('SessionManager.initializeSession', error);
return {
ok: false,
status: SessionStatus.ERROR,
error: `Failed to initialize session: ${String(error)}`,
};
}
}
/**
* Get current QR code for a session.
*/
getQRSnapshot(sessionId: string): string | null {
try {
const qr = qrStorage.get(sessionId);
return qr ? qr.data : null;
} catch (error) {
logger.error('SessionManager.getQRSnapshot', error);
return null;
}
}
/**
* Get session status.
*/
async getSessionStatus(sessionId: string): Promise<SessionStatus | null> {
try {
const session = this.sessionMap.get(sessionId);
if (!session) return null;
const client = this.clientMap.get(sessionId);
if (!client) return null;
const state = await client.getConnectionState();
if (state === SocketState.CONNECTED) {
session.status = SessionStatus.AUTHENTICATED;
session.lastActivity = new Date();
} else if (
state === SocketState.OPENING ||
state === SocketState.PAIRING ||
state === SocketState.UNPAIRED
) {
session.status = SessionStatus.WAITING_QR;
} else {
session.status = SessionStatus.CLOSED;
}
return session.status;
} catch (error) {
logger.error('SessionManager.getSessionStatus', error);
return null;
}
}
/**
* Get client for a session.
*/
getClient(sessionId: string): Whatsapp | null {
try {
const client = this.clientMap.get(sessionId);
if (!client) {
logger.warn('SessionManager.getClient', `Client not found for session ${sessionId}`);
return null;
}
const session = this.sessionMap.get(sessionId);
if (session) {
session.lastActivity = new Date();
}
return client;
} catch (error) {
logger.error('SessionManager.getClient', error);
return null;
}
}
/**
* Close a session.
*/
async closeSession(sessionId: string): Promise<{ ok: boolean; error?: string }> {
try {
logger.info('SessionManager.closeSession', `Closing session ${sessionId}`);
const client = this.clientMap.get(sessionId);
if (client) {
try {
await client.close();
} catch (error) {
logger.warn('SessionManager.closeSession', `Error closing client: ${String(error)}`);
}
}
this.clientMap.delete(sessionId);
this.sessionMap.delete(sessionId);
this.qrListeners.delete(sessionId);
qrStorage.clear(sessionId);
tokenStorage.delete(sessionId);
logger.info('SessionManager.closeSession', `Session ${sessionId} closed`);
return { ok: true };
} catch (error) {
logger.error('SessionManager.closeSession', error);
return { ok: false, error: String(error) };
}
}
/**
* Setup event listeners for a client.
*/
private setupClientListeners(sessionId: string, client: Whatsapp): void {
try {
client.onStateChange((state) => {
logger.info('SessionManager.onStateChange', `Session ${sessionId} state: ${state}`);
const session = this.sessionMap.get(sessionId);
if (!session) return;
if (state === SocketState.CONNECTED) {
session.status = SessionStatus.AUTHENTICATED;
qrStorage.clear(sessionId);
logger.info('SessionManager', `Session ${sessionId} authenticated`);
} else if (
state === SocketState.OPENING ||
state === SocketState.PAIRING ||
state === SocketState.UNPAIRED
) {
session.status = SessionStatus.WAITING_QR;
} else {
session.status = SessionStatus.CLOSED;
}
});
client.onMessage(() => {
logger.debug('SessionManager.onMessage', `Message received in session ${sessionId}`);
});
} catch (error) {
logger.error('SessionManager.setupClientListeners', error);
}
}
/**
* Handle QR code generation.
*/
private handleQRGenerated(sessionId: string, qr: string): void {
try {
logger.info('SessionManager', `QR generated for session ${sessionId}`);
qrStorage.store(sessionId, qr);
const listener = this.qrListeners.get(sessionId);
if (listener) {
listener(qr);
}
} catch (error) {
logger.error('SessionManager.handleQRGenerated', error);
}
}
/**
* Get session info.
*/
getSessionInfo(sessionId: string): Session | null {
return this.sessionMap.get(sessionId) || null;
}
/**
* List all active sessions.
*/
listActiveSessions(): string[] {
return Array.from(this.sessionMap.keys());
}
/**
* Get number of active sessions.
*/
getActiveSessions(): number {
return this.clientMap.size;
}
/**
* Cleanup expired sessions (optional).
*/
cleanupExpiredSessions(): void {
try {
const now = new Date().getTime();
const expiredSessions: string[] = [];
for (const [sessionId, session] of this.sessionMap.entries()) {
const sessionAge = now - session.createdAt.getTime();
if (sessionAge > SESSION_TIMEOUT) {
expiredSessions.push(sessionId);
}
}
for (const sessionId of expiredSessions) {
this.closeSession(sessionId).catch((error) => {
logger.error('SessionManager.cleanupExpiredSessions', error);
});
}
if (expiredSessions.length > 0) {
logger.info('SessionManager', `Cleaned up ${expiredSessions.length} expired sessions`);
}
} catch (error) {
logger.error('SessionManager.cleanupExpiredSessions', error);
}
}
}
export const sessionManager = new SessionManager();