D
Engineering

Session Lifecycle

Siklus hidup sesi Baileys — startSession, status CONNECTING/QR_READY/CONNECTED/DISCONNECTED, auto-reconnect, deleteSession, restore saat boot. instanceId sebagai identitas konsisten.

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:

PenggunaanLokasi
Nama folder kredensialsession/auth/<instanceId>/
Key pada WhatsAppService.sessions (Map in-memory)src/service/whatsappService.ts:68
Nama room Socket.IOsrc/service/socketService.ts:59-63
Kolom id di koleksi devices / admin_devicessrc/model/devices.model.ts

Konsistensi keempatnya wajib dijaga. Jangan mengubah salah satu tanpa memperbarui yang lain.

Status sesi

Status internal di SessionData:

ts
interface SessionData {
  sock: WASocket;
  qr: string | null;
  status: 'CONNECTING' | 'CONNECTED' | 'DISCONNECTED' | 'QR_READY';
}

Untuk konsumsi luar (API + Socket.IO), status dinormalisasi via getSessionStatus():

Status internalNormalisasiisConnected
CONNECTINGclosefalse
QR_READYclosefalse
CONNECTEDopentrue
DISCONNECTEDclosefalse
(tidak ada di Map)NOT_FOUNDfalse

Siklus hidup

text
startSession() → CONNECTING → QR_READY → CONNECTED
                                  ↓
                            DISCONNECTED → auto-reconnect (startSession lagi)
                                         → atau deleteSession() bila loggedOut

startSession(instanceId)

src/service/whatsappService.ts:70-311 — fungsi inti untuk membuat socket Baileys.

Alur

  1. Cek apakah sesi sudah ada di Map — jika CONNECTED, return socket yang ada
  2. Buat folder session/auth/<instanceId>/ jika belum ada
  3. Setup auth state via useMultiFileAuthState(authPath)
  4. Fetch Baileys version via fetchLatestBaileysVersion()
  5. Buat socket via makeWASocket({...}):
    • logger: deviceLogger (Pino child, tagged {session: instanceId})
    • printQRInTerminal: false
    • auth: { creds, keys: makeCacheableSignalKeyStore(...) }
    • browser: ['MyWhatsApp', 'Chrome', '1.0.0']
    • generateHighQualityLinkPreview: true
  6. Simpan ke Map dengan status: 'CONNECTING'
  7. Daftar event listener: creds.update, connection.update, messages.upsert, messages.update

connection.update handler

src/service/whatsappService.ts:117-216 — menangani perubahan status koneksi:

EventAksi
qr tersediaConvert 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

ts
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:

ts
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:

  1. Ambil session dari Map
  2. session.sock.ev.removeAllListeners('creds.update') — matikan listener agar Baileys tidak tulis ke folder yang akan dihapus
  3. await session.sock.logout() — logout dari WhatsApp
  4. session.sock.end(undefined) — tutup socket
  5. this.sessions.delete(instanceId) — hapus dari Map
  6. fs.rmSync(authPath, { recursive: true, force: true }) — hapus folder kredensial

Dipanggil saat:

  • GET /api/logout (manual)
  • connection.update dengan DisconnectReason.loggedOut (WhatsApp logout dari sisi device)

Setelah deleteSession, device butuh scan QR ulang untuk reconnect.

getSocket(instanceId)

ts
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)

ts
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':

ts
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 closeshouldReconnectAksi
Koneksi terputus (jaringan, restart)truestartSession lagi — socket baru, QR baru jika perlu
Logout eksplisit (DisconnectReason.loggedOut)falsedeleteSession — hapus folder kredensial

Sesi bersifat in-memory

ts
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