İçeriğe geç
Node.js

Node.js EventEmitter: Olay Tabanlı Mimari

Tarık Tunç
Node.js EventEmitter: Olay Tabanlı Mimari

Node.js EventEmitter: Olay Tabanlı Mimari

Node.js'in çekirdeğinde yer alan EventEmitter sınıfı, olay tabanlı (event-driven) programlamanın temelini oluşturur. HTTP sunucusu, stream'ler, dosya izleme ve child process'ler dahil olmak üzere Node.js'in birçok yerleşik modülü EventEmitter'dan türetilmiştir. Bu rehberde EventEmitter'ın çalışma prensiplerini, özel event'ler oluşturmayı, hata yönetimini, memory leak önleme stratejilerini ve gerçek dünya kullanım senaryolarını kapsamlı şekilde inceleyeceğiz.

1. EventEmitter Temelleri

EventEmitter, observer (gözlemci) tasarım kalıbının bir uygulamasıdır. Bir nesne olay yayar (emit), bu olaya abone olan tüm listener fonksiyonları sırayla çalıştırılır. Bu decoupled yapı, bileşenler arası iletişimi esnek ve genişletilebilir kılar.

const EventEmitter = require('events');

// Temel kullanım
const emitter = new EventEmitter();

// Event listener ekleme
emitter.on('userLogin', (user) => {
  console.log(`${user.name} giriş yaptı - ${new Date().toISOString()}`);
});

emitter.on('userLogin', (user) => {
  // İkinci listener - analytics kaydı
  console.log(`Analytics: Login event for user ${user.id}`);
});

// Tek seferlik listener
emitter.once('serverStart', (port) => {
  console.log(`Sunucu ${port} portunda başlatıldı`);
});

// Event yayma
emitter.emit('userLogin', { id: 1, name: 'Tarık', email: 'tarik@example.com' });
emitter.emit('serverStart', 3000);
emitter.emit('serverStart', 3000); // once ile eklendiği için bu ikinci çağrı yok sayılır

// Listener kaldırma
const handler = (data) => console.log(data);
emitter.on('test', handler);
emitter.removeListener('test', handler);
// veya: emitter.off('test', handler);

// Tüm listener'ları kaldır
emitter.removeAllListeners('test');

// Listener sayısı ve isimleri
console.log(emitter.listenerCount('userLogin')); // 2
console.log(emitter.eventNames()); // ['userLogin']
Bilgi: EventEmitter'da listener'lar senkron olarak ve eklendikleri sıraya göre çalıştırılır. Eğer bir listener exception fırlatırsa, sonraki listener'lar çalışmaz. Bu nedenle listener'larda try/catch kullanmak önemlidir.

2. Özel EventEmitter Sınıfları Oluşturma

Kendi sınıflarınızda EventEmitter'ı extend ederek domain-specific event sistemleri oluşturabilirsiniz. Bu yaklaşım, iş mantığını olay tabanlı bir mimari ile organize etmenizi sağlar ve loosely coupled bir tasarım sunar.

const EventEmitter = require('events');

class OrderService extends EventEmitter {
  constructor() {
    super();
    this.orders = new Map();
  }

  async createOrder(orderData) {
    const order = {
      id: `ORD-${Date.now()}`,
      ...orderData,
      status: 'pending',
      createdAt: new Date()
    };

    this.orders.set(order.id, order);
    this.emit('order:created', order);
    return order;
  }

  async processPayment(orderId, paymentDetails) {
    const order = this.orders.get(orderId);
    if (!order) throw new Error('Sipariş bulunamadı');

    try {
      this.emit('payment:processing', { orderId, amount: order.total });

      // Ödeme işlemi simülasyonu
      await new Promise(resolve => setTimeout(resolve, 1000));

      order.status = 'paid';
      order.paymentId = `PAY-${Date.now()}`;

      this.emit('payment:completed', {
        orderId,
        paymentId: order.paymentId,
        amount: order.total
      });

      return order;
    } catch (error) {
      order.status = 'payment_failed';
      this.emit('payment:failed', { orderId, error: error.message });
      throw error;
    }
  }

  async shipOrder(orderId) {
    const order = this.orders.get(orderId);
    if (!order || order.status !== 'paid') {
      throw new Error('Sipariş gönderilemez');
    }

    order.status = 'shipped';
    order.shippedAt = new Date();

    this.emit('order:shipped', {
      orderId,
      estimatedDelivery: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000)
    });

    return order;
  }
}

// Kullanım ve event listener'lar
const orderService = new OrderService();

// Email bildirimi
orderService.on('order:created', (order) => {
  console.log(`[EMAIL] Sipariş onayı gönderildi: ${order.id}`);
});

// Stok güncelleme
orderService.on('order:created', (order) => {
  console.log(`[STOCK] Stok güncelleniyor: ${order.items?.length} ürün`);
});

// Kargo bildirimi
orderService.on('order:shipped', (data) => {
  console.log(`[SMS] Kargonuz yola çıktı! Tahmini teslimat: ${data.estimatedDelivery}`);
});

// Ödeme log'u
orderService.on('payment:completed', (data) => {
  console.log(`[LOG] Ödeme alındı: ${data.paymentId} - ${data.amount} TL`);
});

// Hata izleme
orderService.on('payment:failed', (data) => {
  console.error(`[ALERT] Ödeme başarısız: ${data.orderId} - ${data.error}`);
});
Uyarı: Varsayılan olarak bir event'e maksimum 10 listener eklenebilir. Bu sınır aşıldığında Node.js memory leak uyarısı verir. Gerçekten ihtiyacınız varsa emitter.setMaxListeners(n) ile bu sınırı artırabilirsiniz, ancak önce tasarımınızı gözden geçirin.

3. Async EventEmitter Pattern'leri

Standart EventEmitter listener'ları senkron çalışır; async listener'lar için özel pattern'ler gerekir. Async event'ler, veritabanı işlemleri, API çağrıları ve dosya operasyonları gibi asenkron işlemlerin event tabanlı mimaride kullanılmasını sağlar.

const EventEmitter = require('events');

// Async-aware EventEmitter
class AsyncEventEmitter extends EventEmitter {
  async emitAsync(event, ...args) {
    const listeners = this.listeners(event);
    for (const listener of listeners) {
      await listener(...args);
    }
  }

  async emitParallel(event, ...args) {
    const listeners = this.listeners(event);
    await Promise.all(listeners.map(listener => listener(...args)));
  }
}

// Kullanım
const emitter = new AsyncEventEmitter();

emitter.on('process', async (data) => {
  await saveToDatabase(data);
  console.log('Veritabanına kaydedildi');
});

emitter.on('process', async (data) => {
  await sendNotification(data);
  console.log('Bildirim gönderildi');
});

// Sıralı çalıştırma (her listener tamamlandıktan sonra diğeri)
await emitter.emitAsync('process', { id: 1, type: 'order' });

// Paralel çalıştırma (tüm listener'lar aynı anda)
await emitter.emitParallel('process', { id: 2, type: 'notification' });

// EventEmitter'da hata yönetimi
emitter.on('error', (err) => {
  console.error('EventEmitter hatası:', err.message);
});

// 'error' event'ine listener eklenmemişse, Node.js process'i çökertir!
// Bu nedenle her zaman 'error' event'ini dinleyin.
İpucu: Node.js'in events modülündeki on() fonksiyonu ile EventEmitter event'lerini async iterator olarak kullanabilirsiniz: const { on } = require('events'); for await (const [data] of on(emitter, 'data')) { ... }. Bu modern pattern, stream benzeri event işleme sağlar.

4. Memory Leak Önleme ve Best Practices

EventEmitter kullanırken memory leak'ler en sık karşılaşılan sorundur. Listener'lar kaldırılmazsa bellekte birikir ve uygulamanın bellek tüketimi sürekli artar. Özellikle HTTP request handler'larında her istekte listener eklemek tehlikeli bir pattern'dir. AbortController ile listener'ları yönetmek, once() kullanmak ve removeListener/removeAllListeners ile temizlik yapmak önemli pratiklerdir.

EventEmitter'ı kullanırken event isimlerini constant olarak tanımlayın (typo hatalarını önler), error event'ini her zaman dinleyin (dinlenmezse process çöker), listener'ları fonksiyon referansı ile ekleyin (kaldırabilmek için) ve wildcard event pattern'leri için EventEmitter2 gibi kütüphaneleri değerlendirin.

Sonuç

EventEmitter, Node.js'in olay tabanlı mimarisinin temel taşıdır. Observer pattern uygulaması ile decoupled, genişletilebilir ve bakımı kolay sistemler oluşturmanızı sağlar. Özel event sınıfları, async event pattern'leri ve memory leak önleme stratejileri ile production-ready event tabanlı uygulamalar geliştirebilirsiniz.

Kaynaklar

İlgili Yazılar