IlmHamroh
JavaScript Full-stack/30-qism. NestJS40/65-dars21 daqiqa
Mundarija (31)

Hodisalar NestJS'da: @nestjs/event-emitter — emit, @OnEvent va domen hodisalari

Qisqacha: @nestjs/event-emitter — Nest ichidagi hodisalar shinasi (eventemitter2 ustida). Xizmat this.hodisalar.emit("vazifa.bajarildi", v) deydi, boshqa klassdagi @OnEvent("vazifa.bajarildi") metodi esa uni tinglaydi. Xizmat endi kim tinglashini bilmaydi: webhook, SSE, email — hammasi alohida tinglovchi. Uch narsani bilib qo'ying: emit() sinxron (tinglovchi emit qaytishidan oldin ishlaydi), tinglovchidagi xato sukut bo'yicha faqat logga yoziladi, hodisa esa xotirada yashaydi — jarayon o'lsa, yo'qoladi.

Bu darsda

  • EventEmitterModule ni ulab, emit, emitAsync va @OnEvent bilan hodisa chiqarasiz va tinglaysiz.
  • vazifa.* va ** naqshlari qanday ishlashini haqiqiy chiqishda ko'rasiz.
  • Tinglovchidagi xato qachon yutilishini va qachon jarayonni yiqitishini bilasiz.
  • In-process hodisa, outbox va navbat o'rtasida tanlay olasiz.
  • vazifalar-api da webhook'ni xizmatdan ajratib, alohida tinglovchiga o'tkazasiz.

Oldin bilishingiz kerak: EventEmitter chuqur, Rejalashtirish: @nestjs/schedule, Tashqi HTTP'ni mock qilish, Transactional outbox naqshi.

Oldingi darsda eksport tozalash @Interval ga o'tdi. Bu darsda loyiha bosqichining ikkinchi qismi: domen hodisalari. Ular keyingi ikki darsga ham kerak — SSE oqimi aynan shu hodisalarni tinglaydi.

1. Nega bu kerak?

25-qismda vazifalar-api ga webhook qo'shgandik (Tashqi HTTP'ni mock qilish): vazifa bajarilganda tashqi manzilga xabar ketadi. Nest'ga ko'chganda VazifalarService webhook'ni constructor orqali oldi:

ts
constructor(
  @Inject(VAZIFA_OMBORI) private readonly ombor: VazifaOmbori,
  @Inject(LOG) private readonly log: Log,
  @Optional() @Inject(XABARCHI)
  private readonly xabarchi: Xabarchi | null = null,
) {}

Va yangila() ichida o'zi chaqirdi: this.xabarchi("vazifa.bajarildi", v). Bu ishlaydi. Endi kelajakni tasavvur qiling. Ertaga SSE oqimi kerak bo'ladi (to'rtinchi darsda), keyin email, keyin Telegram bot (29-qism integratsiyalari). Har biri uchun xizmatga yangi bog'liqlik qo'shiladi va yangila() uzaya boradi. Biznes qoidalari yozilgan klass — "kimga xabar berish kerak" degan ro'yxatga aylanadi.

Bunday bog'lanishning nomi — qattiq bog'lanish (tight coupling). Xizmat o'z ishini (vazifani saqlash) qilishi bilan birga boshqalarning ishini ham boshqarmoqda.

Yechim — domen hodisasi (domain event): xizmat faqat "nima bo'ldi"ni e'lon qiladi — "vazifa bajarildi". Kim eshitadi, nima qiladi — uning ishi emas.

O'xshatish: mahalla e'lonlar taxtasi. Rais "Shanba kuni hashar" deb e'lon osadi. Kim kelishi, kim ketmon olib kelishi — har kim o'zi biladi. Rais har uyga alohida borib aytmaydi. Yangi qo'shni ko'chib kelsa ham, raisning ishi o'zgarmaydi: qo'shni o'zi taxtani o'qiydi.

2. EventEmitterModule va EventEmitter2

2.1 O'rnatish va ulash

bash
npm i --save-exact @nestjs/event-emitter@12.0.1

Paket ichida eventemitter2 (6.4.9) ishlaydi — Node'ning EventEmitter iga (EventEmitter chuqur) o'xshash, lekin naqshlarni (wildcard) va emitAsync ni biladi.

ts
import { EventEmitterModule } from "@nestjs/event-emitter";

@Module({
  imports: [
    EventEmitterModule.forRoot({ wildcard: true, delimiter: "." }),
  ],
})
export class AppModule {}

forRoot sozlamalari eventemitter2 ga to'g'ridan uzatiladi. Ikkitasi muhim: wildcard: true — vazifa.* kabi naqshlarni yoqadi, delimiter: "." — nom qismlarini nuqta ajratadi. Modul sukut bo'yicha global: EventEmitter2 ni istalgan modulda import qilmasdan olish mumkin.

2.2 emit va @OnEvent

Biz demo loyihada (NestJS 12.2.0, @nestjs/event-emitter 12.0.1) xizmat va tinglovchilarni yozdik:

ts
import { EventEmitter2, OnEvent } from "@nestjs/event-emitter";

@Injectable()
class VazifalarXizmati {
  constructor(private readonly hodisalar: EventEmitter2) {}
  yarat(id: number) {
    console.log(`xizmat: vazifa ${id} saqlandi, emit oldidan`);
    const b = this.hodisalar.emit("vazifa.yaratildi", {
      id, matn: "Non olish",
    });
    console.log(`xizmat: emit qaytdi → ${b}`);
  }
}

@Injectable()
class Tinglovchilar {
  @OnEvent("vazifa.yaratildi")
  webhook(v: { id: number }) {
    console.log(`  webhook tinglovchi: vazifa ${v.id}`);
  }
  @OnEvent("vazifa.*")
  hammasi(v: unknown) {
    console.log(`  vazifa.* tinglovchi: ${JSON.stringify(v)}`);
  }
  @OnEvent("vazifa.yaratildi", { async: true })
  keyin() {
    console.log("  async:true tinglovchi (keyingi tikda)");
  }
}

Xizmat EventEmitter2 ni oddiy provider kabi constructor orqali oladi. Tinglovchi — metodiga @OnEvent qo'yilgan istalgan provider. Ikkalasi ham providers ro'yxatida bo'lishi shart.

x.yarat(7) chaqirdik (Node 24.21.0, biz ishga tushirdik):

text
xizmat: vazifa 7 saqlandi, emit oldidan
  webhook tinglovchi: vazifa 7
  vazifa.* tinglovchi: {"id":7,"matn":"Non olish"}
xizmat: emit qaytdi → true
  async:true tinglovchi (keyingi tikda)

Tartibga diqqat bilan qarang — darsning eng muhim joyi shu:

  1. Ikki oddiy tinglovchi emit qaytishidan oldin ishladi. emit() sinxron: u tinglovchilarni birma-bir chaqiradi va keyin qaytadi.
  2. emit true qaytardi — kamida bitta tinglovchi bor edi.
  3. { async: true } li tinglovchi eng oxirida ishladi: eventemitter2 uni keyingi tikka qoldirdi (event loop navbatiga).

Birinchi punktning oqibati katta. Sinxron tinglovchi uzoq ishlasa (masalan og'ir hisob), u HTTP so'rovni ham ushlab turadi: javob tinglovchi tugagandan keyin ketadi. Shuning uchun tinglovchi tez bo'lsin. Sekin ishni (tarmoqqa so'rov) esa kutmasdan boshlang — loyihadagi webhook xabarchisi aynan shunday: fetch ni boshlaydi va kutmaydi.

Sinxronlikning foydali tomoni ham bor. Tinglovchi shu so'rov kontekstida ishlaydi. nestjs-cls va AsyncLocalStorage qiymatlari unga yetib boradi — logda sorovId saqlanadi. Loyiha qadamida buni ko'ramiz.

2.3 Tuzoq: onModuleInit da emit

Demodagi xizmatga yana bitta metod qo'shgan edik:

ts
onModuleInit() {
  const b = this.hodisalar.emit("vazifa.yaratildi", { id: 0 });
  console.log("onModuleInit ichida emit →", b);
}
text
onModuleInit ichida emit → false

false — tinglovchi topilmadi, garchi webhook va vazifa.* tinglovchilari bor bo'lsa ham. Sabab — vaqt. @nestjs/event-emitter @OnEvent metodlarini onApplicationBootstrap bosqichida ro'yxatga oladi (Lifecycle hook'lar). onModuleInit esa undan oldin keladi. Bu paytda chiqarilgan hodisa hech kimga yetmaydi va jim yo'qoladi.

Ishga tushishda hodisa chiqarish kerak bo'lsa, ro'yxat tayyor bo'lishini kuting. Paketda buning uchun maxsus provider bor:

ts
await app.get(EventEmitterReadinessWatcher).waitUntilReady();

Demoda biz aynan shu qatordan keyin yarat(7) ni chaqirdik.

Tekshirib ko'ring: Xizmat emit("vazifa.yaratildi", v) qildi va keyingi qatorda return v. Tinglovchi ichida 2 soniyalik sinxron sikl bor. HTTP javob qachon ketadi?

Javob

2 soniyadan keyin. emit() tinglovchini sinxron chaqiradi va u tugaguncha qaytmaydi. Javob yuborilishi return v ga, u esa emit ga bog'liq. Shuning uchun tinglovchi og'ir ish qilmasligi kerak. Agar ish chindan og'ir bo'lsa — { async: true } bayrog'i, yoki navbatga (BullMQ) berish.

3. Naqshlar: * va **

3.1 Bitta daraja va ko'p daraja

wildcard: true bilan nomni nuqta bo'laklarga ajratadi. * — aynan bitta bo'lak, ** — istalgan sondagi bo'lak. Demoda ikki darajali hodisa chiqardik va ikki tinglovchi bor edi: vazifa.* (dekorator bilan) va ** (e.on("**", har) bilan qo'lda qo'shilgan):

ts
e.emit("vazifa.izoh.qoshildi", { izoh: "Tez" });
text
--- 2) ikki daraja
  ** tinglovchi: {"izoh":"Tez"}

vazifa.* jim qoldi: vazifa.izoh.qoshildi uch bo'lakli, * esa faqat bitta bo'lakni yopadi. ** esa uni ushladi. Qoida:

Naqsh vazifa.bajarildi vazifa.izoh.qoshildi
vazifa.bajarildi ha yo'q
vazifa.* ha yo'q
vazifa.** ha ha
** ha ha

3.2 Hodisalarni nomlash

Nom — tinglovchi va chiqaruvchi orasidagi shartnoma. Bitta harf xatosi — va tinglovchi jim ishlamay qoladi, hech qanday xato chiqmaydi. Ikki qoida:

  • Shakl: <resurs>.<o'tgan zamondagi fe'l> — vazifa.yaratildi, vazifa.bajarildi. Hodisa — bo'lib o'tgan narsa. "Vazifa yarat" esa buyruq, u boshqa naqsh (keyingi darslardagi CQRS'da ko'ramiz).
  • Nomlarni bitta joyda, o'zgarmas obyekt sifatida saqlang va hamma joyda shundan oling. Loyihada HODISA obyekti shunday: HODISA.bajarildi deb yozilganda xato harf TypeScript'da darrov qizil bo'ladi.

3.3 Tinglovchisiz hodisa

text
--- 3) tinglovchisiz
emit("tolov.keldi") → false
hasListeners("hisob") → true

Tinglovchi bo'lmasa, emit false qaytaradi va boshqa hech narsa bo'lmaydi. hasListeners(nom) — oldindan so'rash usuli. Loyihada undan foydali joyda foydalanamiz: tinglovchi bo'lmasa, qo'shimcha baza o'qishi bajarilmaydi.

4. Tinglovchidagi xato

4.1 Sukut: yutiladi va logga yoziladi

Demoda ikki tinglovchi xato tashladi — biri darhol, biri await dan keyin:

ts
@OnEvent("xato.sinxron")
sinxron() {
  throw new Error("email yuborilmadi");
}

@OnEvent("xato.async")
async asinxron() {
  await kut(5);
  throw new Error("Telegram javob bermadi");
}

Ikkala hodisani emit qildik (biz ishga tushirdik). Chiqishda ikki narsaga qarang: emit nima qaytardi va oxirgi qatorda jarayon tirikmi:

text
--- 4) xato tinglovchida
ERROR [Event] email yuborilmadi
Error: email yuborilmadi
emit(xato.sinxron) → true
emit(xato.async) → true
ERROR [Event] Telegram javob bermadi
Error: Telegram javob bermadi
jarayon tirik

(Izning at … qatorlarini olib tashladik.) Ikkala holatda ham emit odatdagidek true qaytardi, xato chaqiruvchiga yetmadi, jarayon tirik qoldi. Paket har tinglovchini try/catch bilan o'raydi va xatoni Event nomli logger'ga yozadi. Bu sozlamaning nomi suppressErrors va u sukut bo'yicha true.

Bu to'g'ri qaror: email yuborilmagani uchun vazifani saqlash so'rovi 500 bilan yiqilmasligi kerak. Lekin bilib qo'ying — xato faqat logda. Webhook yetib bormaganini mijoz bilmaydi.

4.2 suppressErrors: false — ehtiyot bo'ling

Xatoni chaqiruvchiga qaytarmoqchi bo'lsangiz, { suppressErrors: false } bor. Biz uni alohida faylda sinadik:

ts
@OnEvent("vazifa.bajarildi", { suppressErrors: false })
yubor() {
  throw new Error("webhook manzili topilmadi");
}

// ...
try {
  await e.emitAsync("vazifa.bajarildi", { id: 7 });
} catch (x) {
  console.log("emitAsync ushladi:", (x as Error).message);
}
try {
  console.log("emit →", e.emit("vazifa.bajarildi", { id: 7 }));
} catch (x) {
  console.log("emit ushladi:", (x as Error).message);
}
setTimeout(() => console.log("bu qator chiqmaydi"), 50);
text
emitAsync ushladi: webhook manzili topilmadi
emit → true
Error: webhook manzili topilmadi
    at Qattiq.yubor (file:///…/dist/t-hodisa2.js:16:15)
Node.js v24.21.0

(Iz qisqartirildi.) emitAsync xatoni to'g'ri qaytardi — try/catch ushladi. Oddiy emit esa true qaytardi, catch ishlamadi, keyin esa butun jarayon yiqildi: oxirgi setTimeout chiqmadi.

Nega? Paket har tinglovchini async funksiyaga o'raydi. Ichida xato bo'lsa, o'ram rad etilgan Promise qaytaradi. emit() sinxron va Promise ni kutmaydi — hech kim uni ushlamaydi. Node 24 da ushlanmagan rad etish jarayonni to'xtatadi (ushlanmagan xatolar).

Qoida: suppressErrors: false ni faqat emitAsync bilan ishlating. Oddiy emit bilan u server'ni yiqitadigan bomba.

5. emitAsync — natijani kutish

ts
@OnEvent("hisob")
async birinchi() {
  await kut(20);
  return "email: yuborildi";
}
@OnEvent("hisob")
ikkinchi() {
  return "bot: yuborildi";
}

const natija = await e.emitAsync("hisob");
text
--- 5) emitAsync
["email: yuborildi","bot: yuborildi"] ~30 ms

emitAsync hamma tinglovchilarni chaqiradi, ularning Promise larini kutadi va natijalarni massivda qaytaradi. Tartib — ro'yxatga olingan tartib, tugash tartibi emas: sekin birinchi ham birinchi o'rinda.

Qachon kerak? Asosan testda ("tinglovchi tugaguncha kut") va chaqiruvchi tinglovchining javobiga muhtoj bo'lganda. Lekin ikkinchisidan ehtiyot bo'ling: xizmat tinglovchidan javob kutsa, u yana tinglovchini "biladi" — biz qochgan bog'lanish qaytib keladi. Javob kerak bo'lsa, ehtimol bu hodisa emas, oddiy metod chaqiruvi.

Va oxirgi qator:

text
close dan keyin tinglovchilar: 0

app.close() hamma tinglovchilarni olib tashlaydi (onApplicationShutdown).

6. JS modelida hodisalar shinasi

Hodisalar shinasining yuragi — naqshni moslash va tinglovchilarni ketma-ket chaqirish. Uni paketsiz yozamiz — blok brauzerda ishlaydi:

js
class Hodisalar {
  #royxat = []; // { naqsh, fn }

  on(naqsh, fn) {
    this.#royxat.push({ naqsh, fn });
  }

  // "vazifa.*" — bitta daraja; "**" — hammasi (soddalashtirilgan)
  #mos(naqsh, nom) {
    if (naqsh === "**") return true;
    const n = naqsh.split(".");
    const h = nom.split(".");
    return n.length === h.length
      && n.every((q, i) => q === "*" || q === h[i]);
  }

  emit(nom, data) {
    let bor = false;
    for (const { naqsh, fn } of this.#royxat) {
      if (!this.#mos(naqsh, nom)) continue;
      bor = true;
      try {
        fn(data, nom); // sinxron: emit qaytishidan OLDIN
      } catch (e) {
        console.log(`  ERROR [Event] ${e.message}`); // yutiladi
      }
    }
    return bor;
  }
}

const h = new Hodisalar();
h.on("vazifa.bajarildi", (v) => console.log(`  webhook: ${v.id}`));
h.on("vazifa.*", (v, nom) => console.log(`  sse: ${nom}`));
h.on("vazifa.ochirildi", () => {
  throw new Error("email yuborilmadi");
});

console.log("xizmat: emit oldidan");
console.log("emit →", h.emit("vazifa.bajarildi", { id: 7 }));
console.log("emit →", h.emit("vazifa.ochirildi", { id: 7 }));
console.log("emit →", h.emit("vazifa.izoh.qoshildi", {}));

Konsolda:

text
xizmat: emit oldidan
  webhook: 7
  sse: vazifa.bajarildi
emit → true
  sse: vazifa.ochirildi
  ERROR [Event] email yuborilmadi
emit → true
emit → false

Model Nest chiqishini aynan takrorlaydi: tinglovchilar emit → true dan oldin, xato logga yozildi va emit baribir true, uch bo'lakli nom vazifa.* ga tushmadi (false). Soddalashtirilgan joylari: ** faqat "hammasi" ma'nosida (haqiqiysi vazifa.** ni ham biladi), async tinglovchi va emitAsync yo'q.

flowchart LR
  X["VazifalarService"] -->|"emit vazifa.bajarildi"| E["EventEmitter2"]
  E --> W["WebhookTinglovchi"]
  E --> S["SSE oqimi"]
  E -.-> B["kelajakda: email, bot"]

Diagrammadagi strelkalar yo'nalishiga qarang: xizmatdan faqat bitta strelka chiqadi. Yangi tinglovchi qo'shilsa, chap tomon o'zgarmaydi.

7. In-process hodisa qachon yetmaydi

@nestjs/event-emitter — bitta jarayon ichidagi chaqiruv. Uning chegaralarini bilib qo'ying:

Savol event-emitter Outbox + navbat (29-qism)
Jarayon o'lsa hodisa yo'qoladi bazada saqlanadi
Boshqa server nusxasi eshitmaydi eshitadi
Tranzaksiya rollback hodisa baribir ketgan yozuv bilan birga bekor
Tezlik va soddalik juda tez, paketsiz infra Redis/baza kerak

Uchinchi qator eng nozik. Xizmat yozuvni saqladi, hodisa chiqardi, tinglovchi webhook yubordi — keyin tranzaksiya ROLLBACK bo'ldi. Tashqi tizim mavjud bo'lmagan vazifa haqida xabar oldi. Transactional outbox naqshi shu muammoni hal qiladi: hodisa yozuv bilan bir tranzaksiyada bazaga yoziladi, alohida relay uni keyin yuboradi.

Qoida: "yo'qolsa ham mayli" yoki "bitta jarayon ichida yetarli" bo'lsa — event-emitter. "Albatta yetib borsin" bo'lsa — outbox. Loyihada hozir webhook va SSE event-emitter'da: webhook 25-qismdan beri "fire-and-forget" edi, SSE esa faqat shu daqiqada ulangan mijozlarga. 29-qism integratsiyalari Nest'ga ko'chganda outbox ham ko'chadi (Navbatlar: @nestjs/bullmq darsi).

Tekshirib ko'ring: Server ikki nusxada ishlayapti. Foydalanuvchi A nusxaga ulangan holda SSE oqimini kuzatmoqda. Uning do'sti vazifani B nusxa orqali yaratdi. A dagi oqimda hodisa ko'rinadimi?

Javob

Yo'q. Hodisa B jarayonining EventEmitter2 ida chiqdi; A jarayoni uni bilmaydi. Ko'p nusxada hodisalarni tarqatish uchun umumiy kanal kerak — 29-qismdagi Redis Pub/Sub yoki Streams (Realtime'ni masshtablash). Bu loyihaning yakuniy versiyasida /v1/oqim aynan shunday ishlaydi.

8. Express'da qanday edi → Nest'da qanday

Express (25-qism) Nest
Kim chaqiradi xizmat xabarchi() ni o'zi xizmat emit, tinglovchi @OnEvent
Yangi iste'molchi xizmatni o'zgartirish yangi tinglovchi klass
Ulash ilova.ts da qo'lda Nest topadi (providers)
Testda xizmatga soxta xabarchi xuddi shunday — XABARCHI tokeni

Nima yutdik: xizmat bitta bog'liqlikdan qutuldi, iste'molchilar ko'paysa ham u o'zgarmaydi. Nima yo'qotdik: oqim ko'rinmas bo'ldi. Avval yangila() ni o'qib "bu yerda webhook ketadi" deb ko'rardingiz. Endi webhook boshqa faylda, hodisa nomi orqali bog'langan. "Bu hodisani kim tinglaydi?" savoliga javob — qidiruv (grep "HODISA.bajarildi"). Hodisalar ko'payib ketsa, tizim tushunarsiz bo'ladi — buni hodisa spagettisi deyishadi.

9. Ko'p uchraydigan xatolar

Xato Nima bo'ladi Tuzatish
Tinglovchi providers da yo'q jim ishlamaydi modulga qo'shing
Nomda harf xatosi jim ishlamaydi HODISA obyekti
onModuleInit da emit false, hodisa yo'qoladi waitUntilReady() dan keyin
suppressErrors: false + emit jarayon yiqiladi faqat emitAsync bilan
Og'ir sinxron tinglovchi HTTP javob kechikadi kutmasdan boshlash yoki navbat
Muhim xabar event-emitter'da crash'da yo'qoladi outbox

Birinchi qatorni loyiha qadamida haqiqiy test chiqishi bilan ko'ramiz.

10. Mashqlar

Mashqlar kurs/mashqlar/30/40-hodisalar/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Tinglovchi metodga qo'yiladigan dekorator —
  • emit() tinglovchilarni sinxron chaqiradimi? (ha/yo'q) —
  • vazifa.* naqshi vazifa.izoh.qoshildi ni ushlaydimi? (ha/yo'q) —
  • Tinglovchilar natijasini kutib, massivda qaytaradigan metod —
  • @OnEvent metodlari qaysi lifecycle bosqichida ro'yxatga olinadi? —
Yechim
  • @OnEvent — @OnEvent("vazifa.bajarildi"); provider providers ro'yxatida bo'lishi shart.
  • ha — tinglovchilar emit qaytishidan oldin ishlaydi ({ async: true } dan tashqari).
  • yo'q — * bitta bo'lak; uch bo'lak uchun vazifa.**.
  • emitAsync — ["email: yuborildi","bot: yuborildi"].
  • onApplicationBootstrap — shuning uchun onModuleInit dagi emit false qaytardi.

2-mashq (o'rta): off va hasListeners

Darsdagi Hodisalar modeliga ikki metod qo'shing. off(naqsh, fn) — aynan shu naqsh va funksiya juftligini ro'yxatdan olib tashlasin (SSE mijozi uzilganda kerak). hasListeners(nom) — shu nomga mos tinglovchi bormi, true/false. Ishora: filter va some; funksiyani === bilan solishtiring. Soddalik uchun emit dagi try/catch ni olib tashlang.

Yechim
js
class Hodisalar {
  #royxat = [];

  on(naqsh, fn) {
    this.#royxat.push({ naqsh, fn });
  }

  off(naqsh, fn) {
    this.#royxat = this.#royxat.filter(
      (t) => !(t.naqsh === naqsh && t.fn === fn),
    );
  }

  #mos(naqsh, nom) {
    if (naqsh === "**") return true;
    const n = naqsh.split(".");
    const h = nom.split(".");
    return n.length === h.length
      && n.every((q, i) => q === "*" || q === h[i]);
  }

  hasListeners(nom) {
    return this.#royxat.some((t) => this.#mos(t.naqsh, nom));
  }

  emit(nom, data) {
    let bor = false;
    for (const { naqsh, fn } of this.#royxat) {
      if (!this.#mos(naqsh, nom)) continue;
      bor = true;
      fn(data, nom);
    }
    return bor;
  }
}

const h = new Hodisalar();
const sse = (v, nom) => console.log(`sse: ${nom} ${v.id}`);
h.on("vazifa.*", sse);
console.log(h.hasListeners("vazifa.bajarildi"));
h.emit("vazifa.yaratildi", { id: 1 });
h.off("vazifa.*", sse); // mijoz uzildi
console.log(h.hasListeners("vazifa.bajarildi"));
console.log(h.emit("vazifa.yaratildi", { id: 2 }));

Konsolda:

text
true
sse: vazifa.yaratildi 1
false
false

off funksiyani havola bo'yicha topadi. Shuning uchun h.off("vazifa.*", (v) => …) kabi yangi strelkali funksiya bersangiz, hech narsa o'chmaydi — bu boshqa funksiya. Tinglovchini o'zgaruvchida saqlang (sse). To'rtinchi darsdagi SSE kodi ham tinglovchi o'zgaruvchisini on va off ga beradi.

3-mashq (qiyin): hodisalarni commit'dan keyin yuborish

Darsdagi "rollback" muammosini modellang. Tranzaksiya klassi yozing: uning emit(nom, data) metodi hodisani darhol yubormaydi, ro'yxatga qo'yadi. commit() — ro'yxatdagilarni haqiqiy shinaga yuboradi va ro'yxatni tozalaydi. rollback() — hammasini tashlaydi va nechta tashlanganini chiqaradi. Ishora: ichki massiv [nom, data] juftlari bilan; shina — oddiy Map (nom → funksiyalar ro'yxati) yetarli.

Yechim
js
// Hodisalar tranzaksiya tugagach yuboriladi
class Tinglovchilar {
  #fnlar = new Map(); // nom → [fn]
  on(nom, fn) {
    this.#fnlar.set(nom, [...(this.#fnlar.get(nom) ?? []), fn]);
  }
  emit(nom, data) {
    for (const fn of this.#fnlar.get(nom) ?? []) fn(data);
  }
}

class Tranzaksiya {
  #kutayotgan = [];
  constructor(hodisalar) {
    this.hodisalar = hodisalar;
  }
  emit(nom, data) {
    this.#kutayotgan.push([nom, data]); // hozircha yubormaymiz
  }
  commit() {
    for (const [nom, data] of this.#kutayotgan) {
      this.hodisalar.emit(nom, data);
    }
    this.#kutayotgan = [];
  }
  rollback() {
    const n = this.#kutayotgan.length;
    console.log(`rollback: ${n} hodisa tashlandi`);
    this.#kutayotgan = [];
  }
}

const h = new Tinglovchilar();
h.on("vazifa.yaratildi", (v) => console.log(`webhook: ${v.matn}`));

const t1 = new Tranzaksiya(h);
t1.emit("vazifa.yaratildi", { matn: "Non olish" });
console.log("1-tranzaksiya: hali commit yo'q");
t1.commit();

const t2 = new Tranzaksiya(h);
t2.emit("vazifa.yaratildi", { matn: "Sut olish" });
t2.emit("vazifa.yaratildi", { matn: "Choy damlash" });
t2.rollback(); // baza xato berdi

Konsolda:

text
1-tranzaksiya: hali commit yo'q
webhook: Non olish
rollback: 2 hodisa tashlandi

Webhook commit'dan keyin chiqdi, rollback bo'lgan ikki vazifa haqida esa hech kim bilmadi. Bu yondashuv rollback muammosini hal qiladi, lekin bitta muammo qoladi: commit bo'ldi va hodisa yuborilishidan oldin jarayon o'ldi — hodisa yo'qoldi. Uni faqat outbox hal qiladi: hodisa yozuv bilan birga bazaga tushadi. Ba'zi ORM'larda (masalan MikroORM) "commit'dan keyin" ilgagi tayyor bor.

11. Loyiha qadami: webhook — domen hodisasi tinglovchisiga

11.1 Paket va modul

bash
npm i --save-exact @nestjs/event-emitter@12.0.1

src/app.module.ts — importlar orasiga import { EventEmitterModule } from "@nestjs/event-emitter";, imports massivida ScheduleModule.forRoot() dan oldin:

ts
        // domen hodisalari (wildcard — "vazifa.*" kabi naqsh uchun)
        EventEmitterModule.forRoot({
          wildcard: true, delimiter: ".",
        }),

11.2 VazifalarService — hodisalar

src/vazifalar/vazifalar.service.ts da beshta o'zgarish. Birinchisi — importlar: Optional, Xabarchi turi va XABARCHI tokeni endi kerak emas, EventEmitter2 kerak:

ts
import { Inject, Injectable } from "@nestjs/common";
import { EventEmitter2 } from "@nestjs/event-emitter";
// ...
import { LOG, VAZIFA_OMBORI } from "../tokenlar.ts";

Ikkinchisi — MAKS_VAZIFA dan keyin hodisa nomlari:

ts
/**
 * Domen hodisalari: xizmat KIM tinglashini bilmaydi — webhook
 * (vazifalar.tinglovchi.ts), SSE oqimi, 29-qismda bot va email
 */
export const HODISA = {
  yaratildi: "vazifa.yaratildi",
  yangilandi: "vazifa.yangilandi",
  bajarildi: "vazifa.bajarildi",
  ochirildi: "vazifa.ochirildi",
  almashtirildi: "vazifa.royxat-almashtirildi",
} as const;

Uchinchisi — constructor: xabarchi o'rniga hodisalar:

ts
  constructor(
    @Inject(VAZIFA_OMBORI) private readonly ombor: VazifaOmbori,
    @Inject(LOG) private readonly log: Log,
    // webhook to'g'ridan chaqirilmaydi — hodisa chiqariladi (emit
    // sinxron: tinglovchilar shu so'rov kontekstida, sorovId bilan)
    private readonly hodisalar: EventEmitter2,
  ) {}

To'rtinchisi — yangila. Bu yerda nozik joy bor:

ts
  async yangila(
    id: number,
    ozgarish: VazifaOzgarishi,
    shart?: Shart,
  ): Promise<Vazifa> {
    // Faqat false → true o'tishida "bajarildi": avvalgi holatni
    // bilish kerak — qo'shimcha o'qish faqat tinglovchi bo'lsa
    const kuzatiladi = ozgarish.bajarildi === true
      && this.hodisalar.hasListeners(HODISA.bajarildi);
    const avval = kuzatiladi ? await this.ombor.ol(id) : undefined;
    const v = natija(
      id, await this.ombor.yangila(id, ozgarish, shart),
    );
    this.hodisalar.emit(HODISA.yangilandi, v);
    if (avval?.bajarildi === false && v.bajarildi) {
      this.hodisalar.emit(HODISA.bajarildi, v);
    }
    return v;
  }

"Bajarildi" hodisasi har PATCH {bajarildi: true} da emas, faqat false → true o'tishida chiqishi kerak (25-qismdagi webhook xulqi). Buning uchun avvalgi holatni bilish kerak — bazaga qo'shimcha ol() so'rovi. Bu narx. hasListeners shu narxni kamaytiradi: tinglovchi yo'q bo'lsa, qo'shimcha o'qish ham yo'q. Avval bu shart xabarchi !== null edi; endi xizmat "tinglovchi bormi?" deb so'raydi, "webhook bormi?" deb emas.

Beshinchisi — qolgan yozish metodlariga bittadan emit. yarat da return v dan oldin this.hodisalar.emit(HODISA.yaratildi, v);, ochir oxirida this.hodisalar.emit(HODISA.ochirildi, { id });. almashtirHammasi endi async:

ts
  async almashtirHammasi(
    vazifalar: readonly AlmashtirishVazifasi[],
  ): Promise<{ vazifalar: Vazifa[]; versiya: number }> {
    const n = await this.ombor.almashtirHammasi(vazifalar);
    this.hodisalar.emit(HODISA.almashtirildi, {
      versiya: n.versiya,
      soni: n.vazifalar.length,
    });
    return n;
  }

toplamYarat siklida esa har muvaffaqiyatli element uchun hodisa:

ts
      try {
        const vazifa = await this.ombor.yarat(y);
        this.hodisalar.emit(HODISA.yaratildi, vazifa);
        natijalar.push({ ok: true, vazifa });
      } catch (x) {

Hodisa yozuv muvaffaqiyatli bo'lgandan keyin chiqadi — ombor.yarat xato bersa, emit gacha yetib bormaydi. Yaratilmagan vazifa haqida hech kim xabar olmaydi.

11.3 Tinglovchi

Yangi fayl src/vazifalar/vazifalar.tinglovchi.ts:

ts
// vazifalar.tinglovchi.ts — domen hodisalari tinglovchisi. Vazifa
// bajarilganda webhook (25-qism xulqi) — endi xizmat webhook'ni
// bilmaydi: VazifalarService "vazifa.bajarildi" chiqaradi, shu klass
// tinglaydi. 29-qismda bot va email tinglovchilari shu naqshda
import { Inject, Injectable } from "@nestjs/common";
import { OnEvent } from "@nestjs/event-emitter";
import { XABARCHI } from "../tokenlar.ts";
import type { Xabarchi } from "../umumiy/webhook.ts";
import { HODISA } from "./vazifalar.service.ts";
import type { Vazifa } from "./vazifa.ts";

@Injectable()
export class WebhookTinglovchi {
  constructor(
    @Inject(XABARCHI) private readonly xabarchi: Xabarchi | null,
  ) {}

  // sinxron tinglovchi: emit() uni shu so'rov ichida chaqiradi (CLS
  // konteksti — sorovId logda); xabarchi o'zi fire-and-forget
  // (fetch kutilmaydi)
  @OnEvent(HODISA.bajarildi)
  bajarildi(v: Vazifa): void {
    this.xabarchi?.("vazifa.bajarildi", v);
  }
}

XABARCHI tokeni BogliqliklarModule da allaqachon bor: WEBHOOK_URL berilgan bo'lsa — haqiqiy xabarchi, bo'lmasa — null. ?. — null bo'lsa jim o'tadi.

Bitta e'tibor beradigan joy: tinglovchi null xabarchi bilan ham ro'yxatga olinadi. Demak hasListeners(HODISA.bajarildi) doim true va PATCH {bajarildi: true} doim bitta qo'shimcha ol() qiladi — webhook sozlanmagan bo'lsa ham. Avval bu o'qish faqat WEBHOOK_URL bo'lganda edi. Kanon bu narxni ochiq qabul qilgan: keyingi darsdan SSE ham shu hodisani tinglaydi va tinglovchi baribir bo'ladi.

11.4 Modulga ulash

src/vazifalar/vazifalar.module.ts:

ts
import { WebhookTinglovchi } from "./vazifalar.tinglovchi.ts";
// ...
@Module({
  controllers: [VazifalarController, TeglarController],
  providers: [VazifalarService, WebhookTinglovchi],
  // boshqa modul (masalan eksport) xizmatdan foydalanmoqchi
  // bo'lsa — eksport qilinadi
  exports: [VazifalarService],
})

11.5 Tekshiruv: eski test — yangi arxitektura

Bu qadamda yangi test yozmaymiz. 25-qismdan beri bor test/mock.test.ts webhook xulqini tekshiradi: "faqat false → true da, bitta marta; tashqi server yiqilsa — javob baribir 200". U xizmatga emas, HTTP'ga qarab yozilgan. Arxitektura o'zgardi — test o'zgarmasligi kerak:

bash
npm run tip
npm run lint
npx vitest run test/mock.test.ts
npm test

Biz kanon nusxasida yurgizdik (Node 24.21.0, bazasiz):

text
 ✓ test/mock.test.ts (5 tests) 386ms
      Tests  5 passed (5)
 Test Files  20 passed | 5 skipped (25)
      Tests  78 passed | 6 skipped (84)

Endi tajriba: vazifalar.module.ts da WebhookTinglovchi ni providers dan vaqtincha olib tashladik. tip va lint toza o'tdi — TypeScript buni xato deb bilmaydi. Test esa ushladi:

text
 ❯ test/mock.test.ts (5 tests | 2 failed) 442ms
   × webhook: faqat false → true o'tishida, bitta marta; tana va URL to'g'ri (spy — vi.fn) 266ms
   × webhook: tashqi server yiqilsa — javob baribir 200, xato logda (yutib yuborilmaydi) 93ms
AssertionError: Expected values to be strictly equal:

0 !== 1

Tarjimasi: "qiymatlar qat'iy teng bo'lishi kutilgan edi: 0 teng emas 1". Soxta fetch 1 marta chaqirilishi kerak edi, 0 marta chaqirildi. Hech qanday Nest xatosi, ogohlantirish yo'q — hodisa chiqdi, uni hech kim eshitmadi. "Ko'p uchraydigan xatolar" jadvalining birinchi qatori aynan shu. Uni faqat xulq testi ushlaydi. WebhookTinglovchi ni qaytarib qo'ying.

Hali commit qilmang — salomatlik va SSE keyingi ikki darsda, commit Server-Sent Events NestJS'da darsida.

12. Real ishda

  • Qayerda uchraydi: "buyurtma yaratildi" → email, statistika, kesh tozalash; "foydalanuvchi ro'yxatdan o'tdi" → xush kelibsiz xati, bonus. Nest loyihalarida @nestjs/event-emitter — modullarni bir-biridan ajratishning eng arzon yo'li.
  • Masshtab: hodisa boshqa xizmatga yoki boshqa nusxaga yetishi kerak bo'lsa, jamoalar outbox + broker (RabbitMQ, Kafka) ga o'tadi. Mikroservislardagi hodisalarni Mikroservis asoslari darsida ko'ramiz.
  • Intervyu savollari: "Domen hodisasi nima va nega kerak?", "emit sinxronmi?", "Tinglovchidagi xato chaqiruvchiga yetadimi?", "Hodisa yo'qolmasligi uchun nima qilasiz?".
  • Kod review'da: "bu tinglovchi providers ga qo'shilganmi?" va "bu hodisa tranzaksiya ichida chiqmayaptimi?".

Xulosa

  • EventEmitterModule.forRoot({ wildcard: true, delimiter: "." }) + EventEmitter2.emit + @OnEvent — Nest ichidagi hodisalar.
  • emit() sinxron: tinglovchilar u qaytishidan oldin, shu so'rov kontekstida ishlaydi; { async: true } — keyingi tikda.
  • * — bitta bo'lak, ** — ko'p; nomlar HODISA obyektida.
  • Tinglovchi xatosi sukut bo'yicha logda qoladi; suppressErrors: false faqat emitAsync bilan.
  • In-process hodisa crash'da va boshqa nusxada yo'qoladi — kafolat kerak bo'lsa, outbox.
  • Loyihada xizmat webhook'ni bilmaydi: HODISA chiqaradi, WebhookTinglovchi tinglaydi; eski test o'zgarmay o'tdi.

Keyingi dars: Health check'lar: @nestjs/terminus — /health/live va /health/ready: orkestrator ilova tirik va tayyorligini qanday biladi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Hodisalar NestJS'da: @nestjs/event-emitter — emit, @OnEvent va domen hodisalari — IlmHamroh