Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. EventEmitterModule va EventEmitter2
- 2.1 O'rnatish va ulash
- 2.2 emit va @OnEvent
- 2.3 Tuzoq: onModuleInit da emit
- 3. Naqshlar: * va **
- 3.1 Bitta daraja va ko'p daraja
- 3.2 Hodisalarni nomlash
- 3.3 Tinglovchisiz hodisa
- 4. Tinglovchidagi xato
- 4.1 Sukut: yutiladi va logga yoziladi
- 4.2 suppressErrors: false — ehtiyot bo'ling
- 5. emitAsync — natijani kutish
- 6. JS modelida hodisalar shinasi
- 7. In-process hodisa qachon yetmaydi
- 8. Express'da qanday edi → Nest'da qanday
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): off va hasListeners
- 3-mashq (qiyin): hodisalarni commit'dan keyin yuborish
- 11. Loyiha qadami: webhook — domen hodisasi tinglovchisiga
- 11.1 Paket va modul
- 11.2 VazifalarService — hodisalar
- 11.3 Tinglovchi
- 11.4 Modulga ulash
- 11.5 Tekshiruv: eski test — yangi arxitektura
- 12. Real ishda
- Xulosa
- Manbalar
Hodisalar NestJS'da: @nestjs/event-emitter — emit, @OnEvent va domen hodisalari
Qisqacha:
@nestjs/event-emitter— Nest ichidagi hodisalar shinasi (eventemitter2ustida). Xizmatthis.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 (tinglovchiemitqaytishidan oldin ishlaydi), tinglovchidagi xato sukut bo'yicha faqat logga yoziladi, hodisa esa xotirada yashaydi — jarayon o'lsa, yo'qoladi.
Bu darsda
EventEmitterModuleni ulab,emit,emitAsyncva@OnEventbilan 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-apida 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:
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
npm i --save-exact @nestjs/event-emitter@12.0.1Paket ichida eventemitter2 (6.4.9) ishlaydi — Node'ning EventEmitter iga (EventEmitter chuqur) o'xshash, lekin naqshlarni (wildcard) va emitAsync ni biladi.
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:
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):
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:
- Ikki oddiy tinglovchi
emitqaytishidan oldin ishladi.emit()sinxron: u tinglovchilarni birma-bir chaqiradi va keyin qaytadi. emittrueqaytardi — kamida bitta tinglovchi bor edi.{ async: true }li tinglovchi eng oxirida ishladi:eventemitter2uni 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:
onModuleInit() {
const b = this.hodisalar.emit("vazifa.yaratildi", { id: 0 });
console.log("onModuleInit ichida emit →", b);
}onModuleInit ichida emit → falsefalse — 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:
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 qatordareturn 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):
e.emit("vazifa.izoh.qoshildi", { izoh: "Tez" });--- 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
HODISAobyekti shunday:HODISA.bajarildideb yozilganda xato harf TypeScript'da darrov qizil bo'ladi.
3.3 Tinglovchisiz hodisa
--- 3) tinglovchisiz
emit("tolov.keldi") → false
hasListeners("hisob") → trueTinglovchi 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:
@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:
--- 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:
@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);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
@OnEvent("hisob")
async birinchi() {
await kut(20);
return "email: yuborildi";
}
@OnEvent("hisob")
ikkinchi() {
return "bot: yuborildi";
}
const natija = await e.emitAsync("hisob");--- 5) emitAsync
["email: yuborildi","bot: yuborildi"] ~30 msemitAsync 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:
close dan keyin tinglovchilar: 0app.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:
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:
xizmat: emit oldidan
webhook: 7
sse: vazifa.bajarildi
emit → true
sse: vazifa.ochirildi
ERROR [Event] email yuborilmadi
emit → true
emit → falseModel 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.*naqshivazifa.izoh.qoshildini ushlaydimi? (ha/yo'q) —- Tinglovchilar natijasini kutib, massivda qaytaradigan metod —
@OnEventmetodlari qaysi lifecycle bosqichida ro'yxatga olinadi? —
Yechim
- @OnEvent —
@OnEvent("vazifa.bajarildi"); providerprovidersro'yxatida bo'lishi shart. - ha — tinglovchilar
emitqaytishidan oldin ishlaydi ({ async: true }dan tashqari). - yo'q —
*bitta bo'lak; uch bo'lak uchunvazifa.**. - emitAsync —
["email: yuborildi","bot: yuborildi"]. - onApplicationBootstrap — shuning uchun
onModuleInitdagi emitfalseqaytardi.
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
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:
true
sse: vazifa.yaratildi 1
false
falseoff 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
// 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 berdiKonsolda:
1-tranzaksiya: hali commit yo'q
webhook: Non olish
rollback: 2 hodisa tashlandiWebhook 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
npm i --save-exact @nestjs/event-emitter@12.0.1src/app.module.ts — importlar orasiga import { EventEmitterModule } from "@nestjs/event-emitter";, imports massivida ScheduleModule.forRoot() dan oldin:
// 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:
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:
/**
* 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:
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:
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:
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:
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:
// 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:
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:
npm run tip
npm run lint
npx vitest run test/mock.test.ts
npm testBiz kanon nusxasida yurgizdik (Node 24.21.0, bazasiz):
✓ 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:
❯ 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 !== 1Tarjimasi: "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?", "
emitsinxronmi?", "Tinglovchidagi xato chaqiruvchiga yetadimi?", "Hodisa yo'qolmasligi uchun nima qilasiz?". - Kod review'da: "bu tinglovchi
providersga 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; nomlarHODISAobyektida.- Tinglovchi xatosi sukut bo'yicha logda qoladi;
suppressErrors: falsefaqatemitAsyncbilan. - In-process hodisa crash'da va boshqa nusxada yo'qoladi — kafolat kerak bo'lsa, outbox.
- Loyihada xizmat webhook'ni bilmaydi:
HODISAchiqaradi,WebhookTinglovchitinglaydi; 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
- NestJS hujjati — Events: https://docs.nestjs.com/techniques/events
eventemitter2(wildcard, delimiter, emitAsync): https://github.com/EventEmitter2/EventEmitter2@nestjs/event-emittermanba kodi (12.0.1): https://github.com/nestjs/event-emitter- Martin Fowler, "What do you mean by Event-Driven?": https://martinfowler.com/articles/201701-event-driven.html
- Kanon (IlmHamroh, 2026-10-28): webhook tinglovchiga ko'chdi,
mock.testo'zgarishsiz o'tdi, NestJS 12.2.0,@nestjs/event-emitter12.0.1. Demo (tartib, naqshlar, xato,suppressErrors,emitAsync) va loyiha nusxasidagi tekshiruv, tinglovchisiz test chiqishi — biz ishga tushirdik (2026-11-02, Node 24.21.0, NestJS 12.2.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!