Sesi WhatsApp di engine-whatsapp dikelola oleh WhatsAppService — class statis dengan Map<instanceId, SessionData> in-memory. Setiap sesi adalah satu socket Baileys dengan kredensial persisten di filesystem.
instanceId — identitas konsisten
UUID yang mengidentifikasi satu sesi WhatsApp. Dipakai serentak sebagai:
| Penggunaan | Lokasi |
|---|---|
| Nama folder kredensial | session/auth/<instanceId>/ |
Key pada WhatsAppService.sessions (Map in-memory) | src/service/whatsappService.ts:68 |
| Nama room Socket.IO | src/service/socketService.ts:59-63 |
Kolom id di koleksi devices / admin_devices | src/model/devices.model.ts |
Konsistensi keempatnya wajib dijaga. Jangan mengubah salah satu tanpa memperbarui yang lain.
Status sesi
Status internal di SessionData:
interface SessionData {
sock: WASocket;
qr: string | null;
status: 'CONNECTING' | 'CONNECTED' | 'DISCONNECTED' | 'QR_READY';
}Untuk konsumsi luar (API + Socket.IO), status dinormalisasi via getSessionStatus():
| Status internal | Normalisasi | isConnected |
|---|---|---|
CONNECTING | close | false |
QR_READY | close | false |
CONNECTED | open | true |
DISCONNECTED | close | false |
| (tidak ada di Map) | NOT_FOUND | false |
Siklus hidup
startSession() → CONNECTING → QR_READY → CONNECTED
↓
DISCONNECTED → auto-reconnect (startSession lagi)
→ atau deleteSession() bila loggedOutstartSession(instanceId)
src/service/whatsappService.ts:70-311 — fungsi inti untuk membuat socket Baileys.
Alur
- Cek apakah sesi sudah ada di Map — jika
CONNECTED, return socket yang ada - Buat folder
session/auth/<instanceId>/jika belum ada - Setup auth state via
useMultiFileAuthState(authPath) - Fetch Baileys version via
fetchLatestBaileysVersion() - Buat socket via
makeWASocket({...}):logger: deviceLogger(Pino child, tagged{session: instanceId})printQRInTerminal: falseauth: { creds, keys: makeCacheableSignalKeyStore(...) }browser: ['MyWhatsApp', 'Chrome', '1.0.0']generateHighQualityLinkPreview: true
- Simpan ke Map dengan
status: 'CONNECTING' - Daftar event listener:
creds.update,connection.update,messages.upsert,messages.update
connection.update handler
src/service/whatsappService.ts:117-216 — menangani perubahan status koneksi:
| Event | Aksi |
|---|---|
qr tersedia | Convert ke base64 (qrcode.toDataURL), set status: 'QR_READY', emit qrcode ke room |
connection === 'close' | Cek shouldReconnect (bukan loggedOut). Jika ya → startSession(instanceId) lagi. Jika tidak → deleteSession(instanceId). Update devices/admin_devices (is_connected: 'Disconnected'). Emit device_status: {status: 'close'} |
connection === 'open' | Set status: 'CONNECTED'. Ambil phoneNumber dari sock.user.id. Update devices/admin_devices (no_wa, is_connected: 'Connected'). Emit device_status: {status: 'open'} |
devices vs admin_devices saat update
let deviceMember = await DeviceService.findOne(instanceId);
if (!deviceMember) {
// Fallback ke admin_devices
await DeviceService.updateDeviceAdmin(instanceId, {
no_wa: phoneNumber,
is_connected: 'Connected',
status: 1, // field ekstra admin_devices
});
} else {
await DeviceService.update(instanceId, {
no_wa: phoneNumber,
is_connected: 'Connected',
});
}admin_devices punya field ekstra status: 0|1 (0 = disconnected, 1 = connected).
init() — restore saat boot
src/service/whatsappService.ts:355-382 — dipanggil di app.ts:44 setelah server.listen:
static async init() {
const authBasePath = path.join(import.meta.dirname, '../../session/auth');
if (!fs.existsSync(authBasePath)) return;
const dirents = fs.readdirSync(authBasePath, { withFileTypes: true });
const sessionFolders = dirents
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
for (const instanceId of sessionFolders) {
try {
await this.startSession(instanceId);
await new Promise((resolve) => setTimeout(resolve, 2000)); // jeda 2 detik
} catch (error) {
console.error(`[INIT] Gagal memuat sesi ${instanceId}:`, error);
}
}
}deleteSession(instanceId)
src/service/whatsappService.ts:317-342 — logout & hapus sesi:
- Ambil session dari Map
session.sock.ev.removeAllListeners('creds.update')— matikan listener agar Baileys tidak tulis ke folder yang akan dihapusawait session.sock.logout()— logout dari WhatsAppsession.sock.end(undefined)— tutup socketthis.sessions.delete(instanceId)— hapus dari Mapfs.rmSync(authPath, { recursive: true, force: true })— hapus folder kredensial
Dipanggil saat:
GET /api/logout(manual)connection.updatedenganDisconnectReason.loggedOut(WhatsApp logout dari sisi device)
Setelah deleteSession, device butuh scan QR ulang untuk reconnect.
getSocket(instanceId)
static getSocket(instanceId: string): WASocket | null {
const session = this.sessions.get(instanceId);
if (session && session.status === 'CONNECTED') {
return session.sock;
}
return null;
}Return socket hanya jika CONNECTED. Dipakai semua fungsi kirim pesan — return null berarti sesi tidak aktif.
getSessionStatus(instanceId)
static getSessionStatus(instanceId: string) {
const session = this.sessions.get(instanceId);
if (!session) {
return {
id: instanceId,
isConnected: false,
isActivated: false,
status: 'NOT_FOUND',
message: 'Sesi tidak ditemukan.',
};
}
const user = session.sock?.user;
return {
id: instanceId,
status: session.status === 'CONNECTED' ? 'open' : 'close',
isConnected: session.status === 'CONNECTED' ? true : false,
isActivated: session.status === 'CONNECTED' ? true : false,
message: session.status === 'CONNECTED' ? 'Device Terhubung' : 'Menunggu Koneksi',
user: user ? {
name: user.name,
id: user.id.split(':')[0], // bersihkan dari :d@s.whatsapp.net
} : null,
};
}Dipanggil oleh GET /api/device-info dan saat client join_instance (Socket.IO langsung membalas device_status).
Auto-reconnect
Saat connection === 'close':
const shouldReconnect =
(lastDisconnect?.error as Boom)?.output?.statusCode !==
DisconnectReason.loggedOut;
if (shouldReconnect) {
this.startSession(instanceId); // rekursif — buat socket baru
} else {
this.deleteSession(instanceId); // hapus sepenuhnya
}| Penyebab close | shouldReconnect | Aksi |
|---|---|---|
| Koneksi terputus (jaringan, restart) | true | startSession lagi — socket baru, QR baru jika perlu |
Logout eksplisit (DisconnectReason.loggedOut) | false | deleteSession — hapus folder kredensial |
Sesi bersifat in-memory
private static sessions = new Map<string, SessionData>();Kredensial persisten di filesystem (session/auth/<instanceId>/), bukan di MongoDB. Jadi restart tidak kehilangan sesi — tapi butuh waktu N × 2 detik untuk restore semua.
Alur pesan masuk — singkat
messages.upsert event → IncomingMessageService.handle(sock, messages, instanceId) → processMessage. Detail alur di Message Flow.
Alur status pesan — singkat
messages.update event → mapping status 1..5 → dedupe via Set → cari temp_id di chat_inbox → sendWebhook dengan report: true. Detail di Message Flow.
Langkah berikutnya
- Cara pesan masuk diproses? Baca Message Flow.
- Detail event Socket.IO? Baca Realtime Events.
- Known issues sesi? Baca Tech Debt.