8.23-bob: Push bildirishnoma — Firebase FCM
8-QISM — NestJS (chuqur) · 23-mavzu · Amaliy real mavzu
1. Kirish va motivatsiya
Endi yana bir real mavzu — push bildirishnoma (push notification). Bu — foydalanuvchi ilovani ochmasa ham, uning telefon/brauzeriga darrov xabar yuborish: "Buyurtmangiz yo'lda", "Yangi xabar keldi", "50% chegirma!", "To'lov qabul qilindi". Mobil ilova (Android/iOS) va veb (brauzer) uchun eng kuchli qaytarish (re-engagement) vositasi. SMS (5.18 — pullik), email (8.10 — sekin) bilan bir qatorda, push — bepul, tezkor, vizual kanal. Har mobil/veb ilova push'siz tasavvur qilib bo'lmaydi.
Push'ning "sirli" tomoni — u oddiy HTTP javob emas: server foydalanuvchiga o'zi xabar yuboradi (u so'ramasa ham — server-initiated). Buni Firebase Cloud Messaging (FCM) — Google'ning bepul, platformalararo (Android/iOS/Web) xizmati — hal qiladi. Mexanizm: foydalanuvchi qurilmasi FCM'dan token oladi (qurilma manzili kabi) tokenni sizning backend'ga yuboradi (siz saqlaysiz) siz FCM orqali shu tokenga xabar yuborasiz FCM qurilmaga yetkazadi. Backend'ning vazifasi: token saqlash va FCM'ga xabar yuborish.
Bu bob: push nima va mexanizm (token, FCM), Firebase Admin SDK sozlash, token boshqaruvi (saqlash, yangilash, eskirgan tokenni o'chirish), bitta foydalanuvchiga yuborish, topic (mavzuga obuna — ommaviy), data vs notification xabar turlari, ommaviy yuborish (navbat — 8.22), va boshqa kanallar bilan birlashtirish (multi-channel — SMS/email/push). Bu bob 5.18 (SMS), 8.10 (email), 8.18 (real-time), 8.22 (navbat) bilan bog'liq. Push — mobil/veb ilova ajralmas qismi.
O'xshatish: push bildirishnoma — pochtachi telefoningizga qo'ng'iroq qilishi. Email 8.10-bob — pochta qutingizga xat (siz borib ochasiz — sekin); push — pochtachi telefoningizga darrov xabar beradi (siz ilovani ochmasangiz ham — ekranda chiqadi). FCM — pochta xizmati: siz "bu manzilga (token) xabar yetkazing" deysiz, FCM yetkazadi (qaysi operator/platforma — sizning ishingiz emas). Token — har qurilmaning noyob manzili (telefon raqami kabi — lekin u o'zgarishi mumkin, yangilab turish kerak). Eskirgan manzilga xabar yuborsangiz — qaytib keladi (eskirgan tokenni o'chirasiz).
Nega muhim?
- Eng kuchli re-engagement — foydalanuvchini qaytaradi (ilova ochmasa ham).
- Bepul, tezkor, vizual — SMS'dan arzon, email'dan tez.
- Mobil/veb ajralmas — har ilova push ishlatadi.
- Multi-channel — SMS + email + push birga (8.10, 5.18).
2. Nazariya — chuqur tushuntirish
2.1. Push mexanizmi (token oqimi)
PUSH OQIMI (qanday ishlaydi):
1. Foydalanuvchi ilova/saytni ochadi
2. Qurilma FCM'dan TOKEN so'raydi (ruxsat bilan — qurilma manzili)
3. Qurilma tokenni SIZNING backend'ga yuboradi (siz DB'ga saqlaysiz)
4. Hodisa sodir bo'ladi (buyurtma, xabar)
5. Backend FCM'ga: "bu tokenga xabar yubor"
6. FCM qurilmaga yetkazadi (ekranda chiqadi)
Backend vazifasi: TOKEN saqlash (3) + FCM'ga yuborish (5)Push mexanizmi: qurilma FCM'dan token oladi (qurilma manzili) backend'ga yuboradi (saqlanadi) hodisa bo'lganda backend FCM'ga "bu tokenga xabar" deydi FCM qurilmaga yetkazadi. Backend ikki ish qiladi: token saqlash 2.4-bob va FCM orqali yuborish 2.5-bob. Frontend (qurilma) tokenni oladi va ko'rsatadi (11). FCM — yetkazib berishni hal qiladi (siz operatorlarni bilmaysiz).
2.2. FCM nima va nega
FCM (Firebase Cloud Messaging) — Google'ning bepul push xizmati:
Platformalararo: Android, iOS, Web (bitta API)
Bepul (cheksiz xabar)
Token, topic (mavzu), guruh targeting
Ishonchli yetkazish
Muqobillar:
- Web Push API (VAPID — faqat brauzer, FCM'siz)
- OneSignal, Expo (mobil — qulay qatlam)
- APNs (Apple) — FCM ostida ishlatiladi (iOS)FCM — Google'ning bepul, platformalararo (Android/iOS/Web — bitta API) push xizmati. Eng keng (bepul, ishonchli, token/topic). Muqobillar: Web Push (VAPID — faqat brauzer), OneSignal/Expo (qulay qatlam). FCM — sanoat standarti (mobil + veb birga). iOS uchun APNs (Apple) FCM ostida (siz FCM bilan ishlaysiz, FCM APNs'ga uzatadi). Bu bobda FCM (Firebase Admin SDK — backend).
2.3. Firebase Admin SDK sozlash
import * as admin from "firebase-admin";
@Injectable()
export class FirebaseService implements OnModuleInit {
onModuleInit() {
admin.initializeApp({
credential: admin.credential.cert({ // service account (Firebase Console'dan)
projectId: this.config.get("FIREBASE_PROJECT_ID"),
clientEmail: this.config.get("FIREBASE_CLIENT_EMAIL"),
privateKey: this.config.get("FIREBASE_PRIVATE_KEY").replace(/\\n/g, "\n"), // (8.14)
}),
});
}
get messaging() { return admin.messaging(); }
}Firebase Admin SDK — backend'da FCM bilan ishlash kutubxonasi.
initializeApp+ service account (Firebase Console Project Settings Service Accounts JSON kalit). Kalitlar.envda (8.14, 14 —privateKeyko'p qatorli,\nalmashtirish).admin.messaging()— xabar yuborish. Service account — maxfiy (git'ga emas — to'lov kaliti darajasida muhim). Bu — backend FCM ulanishi.
2.3a. Service account JSON — nima va qayerdan olinadi
SERVICE ACCOUNT JSON (Firebase Console Project Settings
Service Accounts "Generate new private key"):
{
"type": "service_account",
"project_id": "myapp-12345",
"private_key_id": "...",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-xxxx@myapp-12345.iam.gserviceaccount.com",
"client_id": "...",
...
}
Ikki yo'l:
1) Alohida .env qiymatlari: FIREBASE_PROJECT_ID / _CLIENT_EMAIL / _PRIVATE_KEY
(private_key'ni bitta qatorda \n bilan qo'yiladi — 2.3'dagi .replace(/\\n/g, "\n"))
2) Butun JSON'ni GOOGLE_APPLICATION_CREDENTIALS ga fayl yo'li sifatida
(admin.credential.applicationDefault() o'zi o'qiydi) — CI/prod uchun qulayService account JSON — Firebase loyihasiga backend nomidan kirish kaliti (u orqali istagan foydalanuvchiga xabar yuborish mumkin). Uni git'ga qo'ymang (14):
.gitignorega qo'shing, prod'da secret manager (AWS Secrets Manager / GCP Secret Manager) yoki muhit o'zgaruvchilari orqali bering.private_keyko'p qatorli —.envda\nbilan bitta qatorga siqilganda, kod ichida\nni haqiqiy yangi qatorga qaytarish shart (aks holda "Invalid PEM formatted message" xatosi — 6-bo'lim, Xato 2).
2.3b. Modul sifatida DI — forRootAsync (NestJS uslubi)
// firebase.module.ts — qayta ishlatiluvchi global modul (async config — ConfigService bilan)
import { Global, Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import * as admin from "firebase-admin";
export const FIREBASE_APP = "FIREBASE_APP"; // DI token
@Global() // butun ilovaga (har modulda import shart emas)
@Module({
imports: [ConfigModule],
providers: [
{
provide: FIREBASE_APP,
inject: [ConfigService], // async — config tayyor bo'lgach
useFactory: (config: ConfigService): admin.app.App => {
if (admin.apps.length) return admin.app(); // qayta init'ni oldini olish (HMR/test)
return admin.initializeApp({
credential: admin.credential.cert({
projectId: config.get<string>("FIREBASE_PROJECT_ID"),
clientEmail: config.get<string>("FIREBASE_CLIENT_EMAIL"),
privateKey: config.get<string>("FIREBASE_PRIVATE_KEY").replace(/\\n/g, "\n"),
}),
});
},
},
FirebaseService,
],
exports: [FIREBASE_APP, FirebaseService],
})
export class FirebaseModule {}
// firebase.service.ts — app'ni inyeksiya orqali oladi (OnModuleInit shart emas)
@Injectable()
export class FirebaseService {
constructor(@Inject(FIREBASE_APP) private readonly app: admin.app.App) {}
get messaging(): admin.messaging.Messaging { return this.app.messaging(); }
}Async modul config (NestJS uslubi):
admin.initializeAppniuseFactory+inject: [ConfigService]orqali provider qilib qo'yamiz — shunda.envyuklanib bo'lgach, DI konteyner Firebase app'ni bir marta yaratadi vaFirebaseServicega inyeksiya qiladi.@Global()— barcha modul uniimportqilmasdan ishlatadi.if (admin.apps.length)— testda/hot-reload'da qayta init'ni oldini oladi (app/duplicate-appxatosi). Bu 2.3'dagiOnModuleInitvariantidan afzalroq: DI-toza, testda mock qilish oson (provideFIREBASE_APP= soxta obyekt).
2.4. Token boshqaruvi (saqlash, yangilash)
@Entity()
export class DeviceToken {
@PrimaryGeneratedColumn("uuid") id: string;
@Column() userId: string;
@Column({ unique: true }) token: string; // FCM token (qurilma)
@Column() platforma: "android" | "ios" | "web";
@CreateDateColumn() createdAt: Date;
@Column({ nullable: true }) oxirgiIshlatilgan: Date;
}
// Frontend token yuboradi saqlash (kirish/qurilma ulanganda)
async tokenSaqla(userId: string, token: string, platforma: string) {
await this.repo.upsert({ userId, token, platforma }, ["token"]); // bor bo'lsa yangilash
}
// Bir foydalanuvchi — KO'P token (telefon + planshet + brauzer)Token boshqaruvi: har qurilma — bitta token (DB'da
userIdbilan bog'lanadi). Bir foydalanuvchi — ko'p token (telefon + planshet + brauzer — hammasiga yuboriladi).upsert(token bor bo'lsa yangilash — token o'zgaradi). Token yangilanadi (qurilma vaqti-vaqti yangi token beradi — frontend yuboradi). Eskirgan token o'chiriladi (2.6 — yuborganda xato qaytsa). Token — push'ning manzili (to'g'ri boshqarish muhim).
2.5. Bitta foydalanuvchiga yuborish
async foydalanuvchigaYubor(userId: string, sarlavha: string, matn: string, data?: any) {
const tokenlar = await this.tokenRepo.find({ where: { userId } }); // barcha qurilma
if (!tokenlar.length) return;
const message = {
notification: { title: sarlavha, body: matn }, // ko'rinadigan qism (2.7)
data: data || {}, // qo'shimcha ma'lumot (2.7)
tokens: tokenlar.map((t) => t.token),
};
const natija = await this.firebase.messaging.sendEachForMulticast(message); // ko'p token
await this.eskirganTokenlarniOchir(natija, tokenlar); // (2.6)
}Bitta foydalanuvchiga: uning barcha qurilma tokenlarini olib, hammasiga yuborish (
sendEachForMulticast— ko'p token bir so'rovda).notification(ko'rinadigan — sarlavha/matn — 2.7),data(qo'shimcha — bosilganda harakat — 2.7). Natijani tekshirib, eskirgan tokenlarni o'chirish 2.6-bob. Bu — eng keng ishlatiladigan (buyurtma holati, xabar — muayyan foydalanuvchiga).
API tanlovi (
sendvssendEachForMulticastvssendEach):send(message)— bitta manzil (bittatoken, yokitopic, yokicondition); bittamessageIdqaytaradi.sendEachForMulticast({ tokens, ... })— bir xil xabarni ko'p tokenga (max 500) yuboradi vaBatchResponse(successCount,failureCount, tokenlar tartibidagiresponses[]) qaytaradi — har token uchun alohida natija (shuning uchun eskirgan tokenni aniqlash mumkin — 2.6).sendEach(messages[])— har biri turlicha bo'lgan xabarlar massivini yuboradi (max 500). EskisendMulticast/sendAll(bitta HTTP-batch) endi tavsiya etilmaydi —sendEach*variantlari har xabarni mustaqil yuboradi (biri yiqilsa boshqalari yetadi) va aynan shu ishonchliroq. Ko'p qurilmali foydalanuvchi uchun deyarli har doimsendEachForMulticast.
2.6. Eskirgan tokenni o'chirish
async eskirganTokenlarniOchir(natija: any, tokenlar: DeviceToken[]) {
const ochiriladigan: string[] = [];
natija.responses.forEach((resp: any, i: number) => {
if (!resp.success) {
const kod = resp.error?.code;
// Token yaroqsiz/ro'yxatdan o'chgan DB'dan o'chir
if (kod === "messaging/registration-token-not-registered" ||
kod === "messaging/invalid-registration-token") {
ochiriladigan.push(tokenlar[i].token);
}
}
});
if (ochiriladigan.length) {
await this.tokenRepo.delete({ token: In(ochiriladigan) }); // tozalash
}
}Eskirgan token tozalash (muhim, ko'p unutiladi): foydalanuvchi ilovani o'chirsa/token eskirsa — yuborish xato qaytaradi (
token-not-registered). Bu tokenlarni DB'dan o'chirish kerak (aks holda DB eskirgan tokenlar bilan to'ladi, har yuborishda behuda urinish). Yuborish natijasini tekshirib, yaroqsiz tokenlarni tozalash. Bu — token bazasini toza saqlaydi (samaradorlik).
2.7. Notification vs Data xabar
NOTIFICATION xabar (ko'rinadigan):
{ notification: { title, body } }
tizim avtomatik ko'rsatadi (ilova background'da ham)
oddiy bildirishnoma (sarlavha + matn)
DATA xabar (ilova ishlaydi):
{ data: { type: "order", orderId: "123" } }
ilova o'zi qabul qiladi (custom mantiq — qaysi ekran ochish)
background'da ko'rsatilmaydi (ilova ishlovi kerak)
IKKALASI birga (eng keng):
{ notification: {...}, data: {...} }
ko'rsatiladi + bosilganda data bilan harakat (deep link)Notification vs Data: notification (ko'rinadigan — tizim avtomatik ekranda ko'rsatadi — sarlavha/matn); data (ilova ishlovi — qaysi ekran ochish, custom mantiq — background'da ko'rsatilmaydi). Ikkalasi birga (eng keng) — ko'rsatiladi + bosilganda
databilan harakat (deep link — buyurtma sahifasiga).dataqiymatlari string bo'lishi kerak (FCM cheklovi). To'g'ri tanlov — UX (oddiy bildirishnoma vs interaktiv).
2.7a. Foreground / background / quit holatlari
ILOVA HOLATIGA QARAB PUSH XULQI:
FOREGROUND (ilova ochiq, ekranda):
- notification tizim tomonidan AVTOMATIK CHIQMAYDI (Android/Web)
- frontend onMessage() handler'i xabarni oladi qo'lda ko'rsatish
(local notification / in-app banner)
BACKGROUND (ilova fonda, minimallashtirilgan):
- notification bo'lsa tizim avtomatik ekranda ko'rsatadi
- bosilganda ilova ochiladi + data uzatiladi (onNotificationOpenedApp)
QUIT (ilova butunlay yopiq):
- notification bo'lsa tizim ko'rsatadi (backend/OS yetkazadi)
- bosilib ochilganda: getInitialNotification() bilan data o'qiladi
- data-only bo'lsa yetkazilmasligi mumkin (OS ilovani uyg'otmaydi)
XULOSA: ekranda kafolatli ko'rinishi kerak bo'lsa — notification qo'shing.
Foreground'da ham ko'rsatish frontend zimmasida (backend faqat yuboradi).Ilova holati push xulqini belgilaydi va bu backend emas, frontend mas'uliyati — lekin backend to'g'ri payload yuborishi kerak. Kafolatli ko'rinish uchun
notificationbloki shart (data-only quit holatida yetkazilmasligi mumkin). Frontend (11-qism)onMessage,onNotificationOpenedApp,getInitialNotificationhandler'larini sozlaydi; backend faqatnotification+datani to'g'ri to'ldiradi.
2.7b. Platform config bloklari — android / apns / webpush
// Bir xabar — har platformaga o'z sozlamasi bilan (FCM avtomatik to'g'ri platformani tanlaydi)
const message: admin.messaging.Message = {
token: "...",
notification: { title: "Buyurtma yetdi", body: "Kuryer eshik oldida" },
data: { orderId: "123", action: "open_order" }, // string qiymatlar
android: {
priority: "high", // "high" | "normal" — 2.7c
ttl: 3600 * 1000, // ms — 1 soat (yetkazib bermasa o'chadi)
collapseKey: "order_123", // eski xabarni almashtiradi (bir kalit)
notification: {
channelId: "orders", // Android 8+ kanal (majburiy)
sound: "default",
clickAction: "OPEN_ORDER", // Android intent filtri (deep link)
icon: "ic_notification",
color: "#4CAF50",
tag: "order_123", // bir tag — bir bildirishnoma (yangilanadi)
},
},
apns: { // iOS (APNs orqali)
headers: {
"apns-priority": "10", // 10 = darrov, 5 = tejamkor
"apns-collapse-id": "order_123", // iOS collapse
"apns-expiration": String(Math.floor(Date.now() / 1000) + 3600), // TTL (unix sekund)
},
payload: {
aps: {
alert: { title: "Buyurtma yetdi", body: "Kuryer eshik oldida" },
sound: "default",
badge: 1, // ilova ikonkasida raqam
"content-available": 1, // fon uyg'otish (silent — 2.7d)
category: "ORDER_ACTIONS", // interaktiv tugmalar guruhi
},
},
},
webpush: { // brauzer (Service Worker orqali)
headers: { TTL: "3600", Urgency: "high" },
notification: {
icon: "https://myapp.uz/icon-192.png",
badge: "https://myapp.uz/badge.png",
requireInteraction: true, // o'zi yo'qolmaydi (bosilguncha turadi)
},
fcmOptions: { link: "https://myapp.uz/orders/123" }, // bosilganda ochiladigan URL (deep link)
},
};
await this.firebase.messaging.send(message);Platform config bloklari — bitta xabarni har platforma (Android / iOS / Web) uchun moslashtirish. FCM qurilma tokeni qaysi platforma ekanini biladi va tegishli blokni qo'llaydi. Muhim farqlar: Android —
channelId(Android 8+ da majburiy, aks holda ko'rinmaydi),clickAction/tag; iOS/APNs — hamma narsapayload.apsichida (alert,badge,sound,content-available,category), sarlavhalarapns-priority/apns-expiration; Web/webpush —Service Workerorqali,fcmOptions.linkdeep-link uchun. Top-darajadaginotification— umumiy fallback; platform bloki uni ustidan yozadi (override).
2.7c. Priority, TTL, collapse_key
PRIORITY (yetkazish shoshilinchligi):
android.priority: "high" darrov uyg'otadi (buyurtma, xabar, OTP)
android.priority: "normal" batareya tejash (marketing, yangilik — kechiktirilishi mumkin)
apns-priority: "10" (darrov) / "5" (tejamkor)
TTL (Time To Live — yashash muddati):
android.ttl: 3600000 (ms) qurilma offline bo'lsa, 1 soat kutadi; keyin o'chadi
ttl: 0 faqat qurilma HOZIR online bo'lsa yetkaziladi (aks holda tashlab yuboriladi)
vaqtga bog'liq xabar (chat, jonli hodisa) qisqa TTL; buyurtma uzunroq
COLLAPSE KEY (siqish — eskini almashtirish):
android.collapseKey / apns-collapse-id: "chat_5"
bir kalitli xabarlar birlashadi (oxirgisi ko'rinadi)
misol: sport hisobi (0:0 1:0 2:1) — faqat oxirgi hisob (eskilar to'planmaydi)Priority / TTL / collapse_key — yetkazish sifatini boshqaradi. priority: high ni faqat foydalanuvchi darhol ko'rishi kerak bo'lgan xabarlarga bering (marketing uchun
normal— Doze rejimida batareyani tejaydi, aks holda Google throttling qilishi mumkin). TTL — offline qurilma uchun kutish muddati (o'tib ketsa xabar tashlab yuboriladi — eskirgan xabar ko'rsatilmaydi). collapse_key — bir mavzudagi ketma-ket xabarlarni siqadi (oxirgisi qoladi), notification stack'ni to'ldirmaydi.
2.7d. Silent push (fon xabari — data-only)
// Silent push — foydalanuvchiga KO'RINMAYDI, ilova fonda ish bajaradi
// (masalan: yangi ma'lumot sinxronizatsiyasi, badge yangilash, config qayta yuklash)
async silentPush(userId: string, data: Record<string, string>) {
const tokenlar = await this.tokenRepo.find({ where: { userId } });
await this.firebase.messaging.sendEachForMulticast({
tokens: tokenlar.map((t) => t.token),
data, // FAQAT data — notification YO'Q
android: { priority: "high" }, // fonni uyg'otish uchun
apns: {
headers: { "apns-priority": "5", "apns-push-type": "background" }, // iOS silent talabi
payload: { aps: { "content-available": 1 } }, // notification/sound/badge YO'Q
},
});
}Silent push (fon xabari) — ekranda hech narsa ko'rsatmaydi, faqat ilovani fonda uyg'otib ish bajartiradi (ma'lumot sinxronizatsiyasi, badge hisoblagichni yangilash, remote config qayta yuklash). Faqat
data,notificationyo'q. iOS talabchan:apns-push-type: background,apns-priority: 5, faqatcontent-available: 1(agaralert/sound/badgeqo'shsangiz — iOS uni oddiy push deb rad etishi mumkin). iOS silent push'ni cheklaydi (soatiga bir necha marta) — unga tayanib muhim ishni bajarmang.
2.8. Topic (mavzuga obuna — ommaviy)
// Topic — mavzuga obuna (ommaviy yuborish — har token emas)
async topicgaObuna(token: string, topic: string) {
await this.firebase.messaging.subscribeToTopic([token], topic); // "aksiyalar", "yangiliklar"
}
async topicgaYubor(topic: string, sarlavha: string, matn: string) {
await this.firebase.messaging.send({
topic, // barcha obunachilarga (bir so'rov!)
notification: { title: sarlavha, body: matn },
});
}
// "aksiyalar" topic'iga obuna bo'lganlar hammasi xabar oladi (millionlab — bitta so'rov)Topic (mavzu — ommaviy): foydalanuvchilar mavzuga obuna bo'ladi (
aksiyalar,yangiliklar,sport). Bir mavzuga yuborilsa — barcha obunachilar oladi (bitta so'rov — millionlab token o'rniga). Marketing, e'lon, kategoriya bildirishnomasi uchun. Token ro'yxati o'rniga topic — ommaviy uchun samarali (FCM boshqaradi). Foydalanuvchi obunani boshqaradi (sozlamada yoqish/o'chirish).
2.8a. condition — topic'larni mantiqiy birlashtirish
// Bir necha topic'ni AND/OR/NOT bilan birlashtirib yuborish (token ro'yxatisiz)
async shartBoyichaYubor(sarlavha: string, matn: string) {
return this.firebase.messaging.send({
// "aksiyalar"ga obuna VA ("uz" TILIDA yoki "toshkent"da bo'lganlar)
condition: "'aksiyalar' in topics && ('uz' in topics || 'toshkent' in topics)",
notification: { title: sarlavha, body: matn },
});
}
// Operatorlar: && (va), || (yoki), ! (emas), qavslar — max 5 ta topic bir shartda
condition— bir nechta topic'ni mantiqiy ifoda (&&,||,!, qavslar) bilan birlashtirib bitta so'rovda yuborish. Masalan "aksiyalartopic'iga obuna, lekin faqattoshkentregionidagilar" — token ro'yxatini yig'ib, filtrlashsiz. FCM shartni o'zi hisoblab, mos obunachilarga yetkazadi. Bir shartda maksimum 5 ta topic ishlatiladi. Sig'imli segmentatsiya (region + til + kategoriya) uchun kuchli vosita — token bazasi ustidan qo'ldaWHEREyozishdan qutuladi.topic2.8-bob — bitta mavzu;condition— mavzular kombinatsiyasi.
2.8b. Token yangilanishi (rotation) va hayot davri
TOKEN HAYOT DAVRI (nega yangilab turish shart):
- FCM token DOIMIY EMAS — quyidagi holatlarda O'ZGARADI:
• ilova qayta o'rnatilgan / ma'lumoti tozalangan
• token uzoq ishlatilmaganda FCM uni yangilaydi (rotation)
• foydalanuvchi ilova ma'lumotini qo'lda tozalasa
- Frontend `onTokenRefresh` (Android) / `onIdTokenChanged` hodisasida
YANGI tokenni backend'ga qayta yuboradi siz upsert qilasiz 2.4-bob
- `oxirgiIshlatilgan` maydonini har muvaffaqiyatli yuborishda yangilab boring
uzoq faol bo'lmagan (masalan 270 kundan ortiq) tokenlarni davriy tozalang
(FCM ham 270+ kun faolsiz tokenni yaroqsiz deb belgilaydi)// Muvaffaqiyatli yuborishdan keyin faol tokenlar "oxirgiIshlatilgan"ini yangilash
private async faollikBelgila(tokenlar: DeviceToken[], natija: admin.messaging.BatchResponse) {
const faolTokenlar = tokenlar.filter((_, i) => natija.responses[i].success).map((t) => t.token);
if (faolTokenlar.length) {
await this.tokenRepo.update({ token: In(faolTokenlar) }, { oxirgiIshlatilgan: new Date() });
}
}
// Davriy tozalash (cron — 8.21): 270 kundan ortiq faolsiz tokenlarni o'chirish
@Cron("0 3 * * 0") // har yakshanba 03:00
async eskiTokenlarniTozala() {
const chegara = new Date(Date.now() - 270 * 24 * 60 * 60 * 1000);
await this.tokenRepo.delete({ oxirgiIshlatilgan: LessThan(chegara) });
}Token yangilanishi (rotation): FCM tokeni doimiy manzil emas — ilova qayta o'rnatilganda, ma'lumot tozalanganda yoki uzoq ishlatilmaganda FCM uni almashtiradi. Frontend
onTokenRefreshhodisasida yangi tokenni backend'ga qayta yuboradi, sizupsert2.4-bob bilan yangilaysiz. Ikki tozalash mexanizmi birga ishlaydi: (1) reaktiv — yuborishdatoken-not-registeredxatosi qaytsa darrov o'chirish 2.6-bob; (2) proaktiv —oxirgiIshlatilganni har muvaffaqiyatda yangilab, 270+ kun faolsiz tokenlarni cron 8.21-bob bilan davriy o'chirish (FCM ham shu muddatdan keyin tokenni yaroqsiz sanaydi). Bu ikkisi token bazasini toza va samarali saqlaydi.
2.9. Ommaviy yuborish (navbat — 8.22)
// Barcha foydalanuvchiga (token bo'yicha) — navbat + batch (8.22)
async ommaviy(sarlavha: string, matn: string) {
const tokenlar = await this.tokenRepo.find();
// FCM limit: 500 token / sendEachForMulticast batch
for (let i = 0; i < tokenlar.length; i += 500) {
const batch = tokenlar.slice(i, i + 500).map((t) => t.token);
await this.pushQueue.add("send-batch", { tokens: batch, sarlavha, matn }); // navbat (8.22)
}
}
// Topic afzal 2.8-bob — ommaviy uchun; token batch — aniq nazorat kerak bo'lsaOmmaviy yuborish: token bo'yicha barcha foydalanuvchiga FCM limit (500 token/
sendEachForMulticast) batch + navbat (8.22 — rate limit, retry). Ommaviy uchun topic afzal (2.8 — bitta so'rov); token batch — aniq nazorat (faqat aktiv, faqat regiongacha) kerak bo'lganda. Webhook/broadcast kabi (8.12: 2.22) — navbat orqali ishonchli. Eskirgan tokenlar tozalanadi 2.6-bob.
2.9a. Batch limit, rate limit va throttling
FCM CHEKLOVLARI:
- sendEachForMulticast: bir so'rovda MAX 500 token
(500'dan ko'p bo'lsa — "messaging/invalid-argument", batch kerak — 2.9)
- data payload: max 4 KB (notification+data birga)
- topic yuborish: bir loyihada minutiga cheklangan (topic'ga tez-tez yuborish throttling)
- APNs/Web push provayder darajasida ham rate limit qo'yishi mumkin
THROTTLING (o'z tarafingizdan sekinlashtirish):
- navbat (BullMQ — 8.22) rate limiter: { max: N, duration: 1000 } (sekundiga N ish)
- batch'lar orasida biroz pauza (FCM'ni ko'mib tashlamaslik)
- retry backoff: vaqtinchalik xato (messaging/internal-error, unavailable) qayta urinBatch/rate limit:
sendEachForMulticastbir so'rovda 500 tokenni qabul qiladi — undan ortiq bo'lsa 500'talik bo'laklarga bo'ling 2.9-bob. Payload 4 KB bilan cheklangan (kattadatayubormang — faqat ID/yo'nalish, to'liq ma'lumotni ilova API'dan oladi). Ommaviy yuborishda navbat 8.22-bob rate limiter'i bilan sekundiga yuboriladigan so'rovlar sonini cheklang — FCM'ni yoki APNs'ni "ko'mib" qo'ymaslik uchun. Vaqtinchalik xatolarni (internal-error,server-unavailable) eksponensial backoff bilan qayta urining; doimiy xatolarni (token-not-registered) qayta urinmang — tokenni o'chiring 2.6-bob.
2.9b. Messaging xato kodlari (to'liq boshqaruv)
// FCM yuborishda qaytadigan asosiy error.code lar va harakat
function xatoHarakat(kod?: string): "ochir" | "qaytaUrin" | "tekshir" {
switch (kod) {
// TOKEN YAROQSIZ DB'dan O'CHIR (2.6)
case "messaging/registration-token-not-registered": // ilova o'chirilgan / token eskirgan
case "messaging/invalid-registration-token": // buzuq token
case "messaging/invalid-argument": // token formati noto'g'ri (yakka token uchun)
return "ochir";
// VAQTINCHALIK BACKOFF BILAN QAYTA URIN
case "messaging/internal-error":
case "messaging/server-unavailable":
case "messaging/unavailable":
case "messaging/quota-exceeded": // rate limit — sekinroq qayta urin
return "qaytaUrin";
// SOZLAMA/KIRISH XATOSI LOG + OGOHLANTIR (kod/config tuzatish kerak)
case "messaging/authentication-error": // service account noto'g'ri (2.3)
case "messaging/mismatched-credential": // token boshqa loyihaga tegishli
case "messaging/message-rate-exceeded":
default:
return "tekshir";
}
}Xato kodlari (
error.code) — yuborish natijasidagi har javobda keladi va uch guruhga ajratiladi: (1) token yaroqsiz (registration-token-not-registered,invalid-registration-token) DB'dan o'chirish 2.6-bob; (2) vaqtinchalik (internal-error,server-unavailable,quota-exceeded) backoff bilan qayta urinish; (3) sozlama/kirish (authentication-error— service account xato,mismatched-credential— token boshqa loyihaniki) log + ogohlantirish (kod/config tuzatiladi). To'g'ri klassifikatsiya token bazasini toza saqlaydi va behuda qayta urinishlarni oldini oladi.
2.10. Multi-channel (SMS + email + push birga)
// Bildirishnoma — bir necha kanal (foydalanuvchi tanloviga qarab)
interface XabarKanali { yubor(user: User, xabar: any): Promise<void>; } // Strategy (9.2)
@Injectable()
export class NotificationService {
constructor(
private push: PushKanal, // FCM (bu bob)
private sms: SmsKanal, // (5.18)
private email: EmailKanal, // (8.10)
) {}
async yubor(userId: string, xabar: any) {
const user = await this.usersService.bitta(userId);
const kanallar = user.bildirishnomaSozlamalari; // foydalanuvchi tanlovi
if (kanallar.push) await this.push.yubor(user, xabar);
if (kanallar.sms) await this.sms.yubor(user, xabar); // muhim (to'lov)
if (kanallar.email) await this.email.yubor(user, xabar);
}
}Multi-channel: push — bitta kanal (SMS — 5.18, email — 8.10 bilan birga). To'g'ri yondashuv — foydalanuvchi tanloviga qarab kanal (sozlamada — push/SMS/email yoqish), va xabar muhimligiga (oddiy push; kritik SMS ham). Strategy pattern (9.2 — har kanal alohida). Push bepul (birinchi tanlov), SMS pullik (faqat kritik — to'lov, OTP). Birlashgan bildirishnoma tizimi — professional.
2.10a. Web Push — VAPID bilan (FCM'siz muqobil)
// Brauzer push'ni FCM'siz ham qilish mumkin — `web-push` kutubxonasi (VAPID standarti)
// npm i web-push ; VAPID kalit juftini bir marta generatsiya: webpush.generateVAPIDKeys()
import * as webpush from "web-push";
@Injectable()
export class WebPushService implements OnModuleInit {
onModuleInit() {
webpush.setVapidDetails(
"mailto:admin@myapp.uz", // aloqa (majburiy)
this.config.get("VAPID_PUBLIC_KEY"), // frontend'ga beriladi (obuna uchun)
this.config.get("VAPID_PRIVATE_KEY"), // maxfiy (.env — 14)
);
}
// Frontend PushManager.subscribe() dan olingan obyektni saqlaydi (endpoint + keys)
async yubor(subscription: webpush.PushSubscription, sarlavha: string, matn: string) {
try {
await webpush.sendNotification(
subscription,
JSON.stringify({ title: sarlavha, body: matn, url: "/orders/123" }),
{ TTL: 3600, urgency: "high" },
);
} catch (e: any) {
if (e.statusCode === 404 || e.statusCode === 410) { // Gone — obuna eskirgan
await this.subRepo.delete({ endpoint: subscription.endpoint }); // tozalash (2.6 kabi)
} else throw e;
}
}
}Web Push (VAPID) — brauzer push'ini FCM'siz, ochiq W3C standarti (Web Push Protocol) orqali amalga oshirish yo'li.
web-pushkutubxonasi VAPID kalit juftini ishlatadi: public key frontend'daPushManager.subscribe()ga beriladi, private key backend'da maxfiy saqlanadi. Frontend obuna obyektini (endpoint+keys— bu FCM tokenining ekvivalenti) backend'ga yuboradi, siz saqlaysiz va shunga xabar yuborasiz.410 Gone/404— obuna eskirgan (FCM'dagitoken-not-registeredkabi — DB'dan o'chiring). Faqat brauzer kerak bo'lsa (mobil ilova yo'q) FCM'siz yengilroq muqobil; lekin Android/iOS ilova ham bo'lsa — FCM bitta API bilan hammasini qamrab olgani afzal.
2.10b. Expo push (React Native muqobil — qisqacha)
// React Native + Expo bo'lsa — Expo o'z push xizmatini beradi (FCM/APNs'ni o'zi boshqaradi)
// npm i expo-server-sdk
import { Expo } from "expo-server-sdk";
const expo = new Expo();
async expoYubor(expoTokens: string[], sarlavha: string, matn: string) {
const messages = expoTokens
.filter((t) => Expo.isExpoPushToken(t)) // "ExponentPushToken[...]" formati
.map((to) => ({ to, title: sarlavha, body: matn, sound: "default", data: { turi: "order" } }));
const chunks = expo.chunkPushNotifications(messages); // 100'talik (Expo limit)
for (const chunk of chunks) {
const tickets = await expo.sendPushNotificationsAsync(chunk);
// keyin getPushNotificationReceiptsAsync bilan "DeviceNotRegistered" tokenlarni tozalash
}
}Expo push (React Native muqobil) — Expo ilovalari uchun Expo o'z push serveri orqali FCM va APNs'ni yashiradi: siz
ExponentPushToken[...]formatli tokenlarga yuborasiz. Payload soddaroq (to/title/body/data), limit — bir chunk'da 100 xabar. Yetkazishni ikki bosqichda tekshiradi: ticket (darrov) va receipt (keyinroq —DeviceNotRegisteredbo'lsa tokenni tozalang). Bu — faqat Expo/React Native uchun qulay qatlam; sof native yoki veb bo'lsa to'g'ridan-to'g'ri FCM ishlating.
2.10c. Xavfsizlik (token maxfiyligi, service account)
PUSH XAVFSIZLIGI (14):
Service account JSON — GIT'GA EMAS (.gitignore + secret manager)
(u orqali istagan foydalanuvchiga xabar yuborish mumkin — to'lov kaliti darajasi)
VAPID private key — maxfiy (.env); public key ochiq (frontend'ga beriladi)
Token endpoint (POST /devices/token) — JWT guard bilan himoyalangan (o'zining userId'siga bog'lanadi)
Aks holda: hujumchi begona foydalanuvchi tokenini o'z hisobiga bog'lab, uning push'ini "o'g'irlaydi"
Data payload'da MAXFIY ma'lumot yubormang (parol, to'liq karta) — faqat ID/yo'nalish
(push tarmoq orqali FCM/APNs serverlaridan o'tadi)
Token yuborishda validatsiya (DTO — IsString, platforma IsIn) — 5.4
Rate limit (bir foydalanuvchiga push spam qilib bo'lmasin — 8.5 throttler)Xavfsizlik (14): eng muhimi — service account JSON hech qachon git'ga tushmasligi (u FCM'ga to'liq kirish beradi). Token ro'yxatga olish endpoint'i JWT guard bilan himoyalanadi va token kirgan foydalanuvchining
userId'siga bog'lanadi (aks holda begona push'ni o'g'irlash mumkin). Push payload maxfiy emas (FCM/APNs serverlaridan o'tadi) — parol, to'liq karta raqami, OTP kodinidataga qo'ymang, faqat ID/yo'nalish yuboring, maxfiy ma'lumotni ilova himoyalangan API'dan alohida oladi. VAPID private key va service account —.env/secret manager'da.
2.11. Best practices (push)
Token DB'da, bir user ko'p token (saqlash/yangilash — 2.4)
Eskirgan tokenni o'chirish (yuborganda xato tozalash — 2.6)
notification + data birga (ko'rsatish + deep link — 2.7)
Topic (ommaviy — samarali — 2.8); token batch (aniq — 2.9)
Ommaviy navbat + batch 500 (8.22 — 2.9)
Service account .env (maxfiy — 8.14, 14, 2.3)
Foydalanuvchi tanlovi (sozlama — yoqish/o'chirish — 2.10)
Multi-channel (push+SMS+email — muhimlikka qarab — 2.10)
Spam qilmang (ortiqcha push ilova o'chiriladi)3. Sintaksis — tez ma'lumotnoma
// Sozlash 2.3-bob: admin.initializeApp({ credential: admin.credential.cert({...}) })
// Bitta token: messaging.send({ token, notification, data })
// Ko'p token 2.5-bob: messaging.sendEachForMulticast({ tokens, notification, data }) // max 500
// Topic 2.8-bob: subscribeToTopic([token], topic); send({ topic, notification })
// Shart bo'yicha (2.8a): send({ condition: "'aksiyalar' in topics && 'uz' in topics" }) // max 5 topic
// Dry-run test (Misol 11): send(message, true) // validateOnly — yetkazmaydi
// Xabar turi 2.7-bob: { notification: { title, body }, data: { key: "value" } } // data — string!
// Platform (2.7b): { android: {...}, apns: { payload: { aps: {...} } }, webpush: {...} }
// Priority/TTL (2.7c): android: { priority: "high", ttl: 3600000, collapseKey }
// Silent (2.7d): { data, apns: { headers: { "apns-push-type": "background" },
// payload: { aps: { "content-available": 1 } } } } // notification YO'Q
// Web Push (2.10a): webpush.sendNotification(subscription, JSON.stringify(payload), opts)
// Expo (2.10b): expo.sendPushNotificationsAsync(chunk) // ExponentPushToken[...]4. Batafsil kod namunalari
Misol 1 — Firebase service (sozlash — 2.3)
@Injectable()
export class FirebaseService implements OnModuleInit {
private app: admin.app.App;
constructor(private config: ConfigService) {}
onModuleInit() {
if (!admin.apps.length) {
this.app = admin.initializeApp({
credential: admin.credential.cert({
projectId: this.config.get("FIREBASE_PROJECT_ID"),
clientEmail: this.config.get("FIREBASE_CLIENT_EMAIL"),
privateKey: this.config.get("FIREBASE_PRIVATE_KEY").replace(/\\n/g, "\n"),
}),
});
}
}
get messaging() { return admin.messaging(); }
}Misol 2 — Token controller (frontend yuboradi — 2.4)
@Controller("devices")
@UseGuards(JwtAuthGuard)
export class DevicesController {
constructor(private pushService: PushService) {}
@Post("token")
async tokenSaqla(@Body() dto: SaveTokenDto, @CurrentUser() user) {
await this.pushService.tokenSaqla(user.id, dto.token, dto.platforma);
return { success: true };
}
@Delete("token")
async tokenOchir(@Body() dto: { token: string }) {
await this.pushService.tokenOchir(dto.token); // logout/o'chirish
return { success: true };
}
}
class SaveTokenDto {
@IsString() token: string;
@IsIn(["android", "ios", "web"]) platforma: string;
}Misol 3 — Push service (token + yuborish — 2.4, 2.5, 2.6)
@Injectable()
export class PushService {
constructor(
private firebase: FirebaseService,
@InjectRepository(DeviceToken) private tokenRepo: Repository<DeviceToken>,
) {}
async tokenSaqla(userId: string, token: string, platforma: string) {
await this.tokenRepo.upsert(
{ userId, token, platforma: platforma as any, oxirgiIshlatilgan: new Date() },
["token"],
);
}
async foydalanuvchigaYubor(userId: string, sarlavha: string, matn: string, data?: Record<string, string>) {
const tokenlar = await this.tokenRepo.find({ where: { userId } });
if (!tokenlar.length) return { yuborildi: 0 };
const natija = await this.firebase.messaging.sendEachForMulticast({
tokens: tokenlar.map((t) => t.token),
notification: { title: sarlavha, body: matn },
data: data || {},
android: { priority: "high" },
apns: { payload: { aps: { sound: "default" } } }, // iOS ovoz
});
await this.eskirganOchir(natija, tokenlar); // tozalash (2.6)
return { yuborildi: natija.successCount, xato: natija.failureCount };
}
private async eskirganOchir(natija: admin.messaging.BatchResponse, tokenlar: DeviceToken[]) {
const ochiriladigan = natija.responses
.map((r, i) => (!r.success && this.eskirganmi(r.error?.code) ? tokenlar[i].token : null))
.filter(Boolean) as string[];
if (ochiriladigan.length) await this.tokenRepo.delete({ token: In(ochiriladigan) });
}
private eskirganmi(kod?: string) {
return kod === "messaging/registration-token-not-registered" ||
kod === "messaging/invalid-registration-token";
}
}Misol 4 — Topic obuna va yuborish (2.8)
async topicgaObuna(userId: string, topic: string) {
const tokenlar = await this.tokenRepo.find({ where: { userId } });
await this.firebase.messaging.subscribeToTopic(tokenlar.map((t) => t.token), topic);
}
async topicdanChiq(userId: string, topic: string) {
const tokenlar = await this.tokenRepo.find({ where: { userId } });
await this.firebase.messaging.unsubscribeFromTopic(tokenlar.map((t) => t.token), topic);
}
async topicgaYubor(topic: string, sarlavha: string, matn: string, data?: any) {
return this.firebase.messaging.send({
topic,
notification: { title: sarlavha, body: matn },
data: data || {},
});
}
// Marketing: topicgaYubor("aksiyalar", "Black Friday!", "50% chegirma") — barcha obunachigaMisol 5 — Deep link (data + bosilganda harakat — 2.7)
// Buyurtma holati — bosilganda buyurtma sahifasiga (deep link)
async buyurtmaHolati(userId: string, orderId: string, holat: string) {
await this.foydalanuvchigaYubor(
userId,
"Buyurtma yangilandi",
`Buyurtmangiz holati: ${holat}`,
{
type: "order", // data (string!)
orderId: orderId, // ilova: /orders/:id ochadi
action: "open_order",
},
);
}
// Frontend (11): notification bosilganda data.type/orderId bilan navigatsiyaMisol 6 — Ommaviy yuborish (navbat + batch — 2.9)
@Injectable()
export class BroadcastService {
constructor(
@InjectRepository(DeviceToken) private tokenRepo: Repository<DeviceToken>,
@InjectQueue("push") private pushQueue: Queue,
) {}
async ommaviy(sarlavha: string, matn: string) {
const jami = await this.tokenRepo.count();
const batch = 500; // FCM limit
for (let offset = 0; offset < jami; offset += batch) {
const tokenlar = await this.tokenRepo.find({ skip: offset, take: batch });
await this.pushQueue.add("batch", { // navbat (8.22)
tokens: tokenlar.map((t) => t.token), sarlavha, matn,
});
}
return { jami };
}
}
@Processor("push")
export class PushProcessor extends WorkerHost {
constructor(private firebase: FirebaseService) { super(); }
async process(job: Job) {
await this.firebase.messaging.sendEachForMulticast({
tokens: job.data.tokens,
notification: { title: job.data.sarlavha, body: job.data.matn },
});
}
}Misol 7 — Multi-channel notification (2.10, Strategy 9.2)
@Injectable()
export class NotificationService {
constructor(
private push: PushService,
private sms: SmsService, // (5.18)
private mail: MailService, // (8.10)
private usersService: UsersService,
) {}
async yubor(userId: string, turi: string, sarlavha: string, matn: string) {
const user = await this.usersService.bitta(userId);
const sozlama = user.bildirishnomaSozlamalari || { push: true, sms: false, email: true };
const ishlar: Promise<any>[] = [];
if (sozlama.push) ishlar.push(this.push.foydalanuvchigaYubor(userId, sarlavha, matn, { turi }));
if (sozlama.email) ishlar.push(this.mail.yubor(user.email, sarlavha, matn));
// SMS — faqat kritik (to'lov, OTP — pullik)
if (turi === "kritik") ishlar.push(this.sms.yubor(user.telefon, matn));
await Promise.allSettled(ishlar); // bittasi yiqilsa, boshqasi davom (8.10)
}
}Misol 8 — Bildirishnoma sozlamalari (foydalanuvchi tanlovi — 2.10)
@Entity()
export class NotificationSettings {
@PrimaryColumn() userId: string;
@Column({ default: true }) push: boolean;
@Column({ default: false }) sms: boolean;
@Column({ default: true }) email: boolean;
// Kategoriyalar (topic)
@Column({ default: true }) aksiyalar: boolean;
@Column({ default: true }) buyurtmalar: boolean;
}
@Patch("notification-settings")
@UseGuards(JwtAuthGuard)
async sozlamaYangila(@Body() dto: UpdateSettingsDto, @CurrentUser() user) {
await this.service.yangila(user.id, dto);
// Topic obunani moslash (2.8)
if (dto.aksiyalar === false) await this.pushService.topicdanChiq(user.id, "aksiyalar");
if (dto.aksiyalar === true) await this.pushService.topicgaObuna(user.id, "aksiyalar");
}Misol 9 — Bildirishnoma tarixi (DB — in-app)
// Push'dan tashqari — ilova ichida bildirishnoma tarixi (DB)
@Entity()
export class Notification {
@PrimaryGeneratedColumn("uuid") id: string;
@Column() userId: string;
@Column() sarlavha: string;
@Column() matn: string;
@Column({ default: false }) oqilgan: boolean; // o'qildi/o'qilmadi
@Column("jsonb", { nullable: true }) data: any;
@CreateDateColumn() createdAt: Date;
}
// Yuborishda — push + DB'ga saqlash (ilova ichida ko'rish uchun)
async yuborVaSaqla(userId: string, sarlavha: string, matn: string, data?: any) {
await this.notifRepo.save({ userId, sarlavha, matn, data }); // tarix (in-app)
await this.pushService.foydalanuvchigaYubor(userId, sarlavha, matn, data); // push
await this.realtimeGateway.yubor(userId, { sarlavha, matn }); // WebSocket (8.18 — agar online)
}Misol 10 — To'liq bildirishnoma modul
src/notifications/
├── firebase.service.ts (FCM sozlash — Misol 1)
├── push.service.ts (token + yuborish — Misol 3)
├── notification.service.ts (multi-channel — Misol 7)
├── devices.controller.ts (token saqlash — Misol 2)
├── push.processor.ts (ommaviy navbat — Misol 6)
├── entities/ (device-token, notification, settings)
└── notifications.module.ts
Oqim:
- Hodisa (buyurtma — 8.19) NotificationService.yubor
- kanal tanlovi (push/SMS/email — sozlama) FCM/Eskiz/SMTP
- DB tarix (in-app) + WebSocket (online bo'lsa — 8.18)
- Ommaviy: topic 2.8-bob yoki navbat batch (2.9)Misol 11 — Test qilish (dry-run + unit mock)
// 1) DRY-RUN — haqiqiy yubormasdan payload'ni FCM'da tekshirish (validateOnly)
// Rivojlanish/CI'da xabar tuzilishini sinash uchun (foydalanuvchi push OLMAYDI)
async yuborTest(token: string) {
await this.firebase.messaging.send(
{ token, notification: { title: "Test", body: "Sinov" } },
/* dryRun */ true, // FCM validatsiya qiladi, YETKAZMAYDI
);
}
// 2) UNIT TEST — Firebase'ni mock qilish (haqiqiy FCM'ga bormaydi)
describe("PushService", () => {
const messagingMock = {
sendEachForMulticast: jest.fn().mockResolvedValue({
successCount: 1, failureCount: 1,
responses: [
{ success: true },
{ success: false, error: { code: "messaging/registration-token-not-registered" } },
],
}),
};
let service: PushService, tokenRepo: any;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
PushService,
{ provide: FirebaseService, useValue: { messaging: messagingMock } },
{ provide: getRepositoryToken(DeviceToken), useValue: {
find: jest.fn().mockResolvedValue([
{ token: "yaxshi-token" }, { token: "eskirgan-token" },
]),
delete: jest.fn(),
} },
],
}).compile();
service = module.get(PushService);
tokenRepo = module.get(getRepositoryToken(DeviceToken));
});
it("eskirgan tokenni o'chiradi", async () => {
await service.foydalanuvchigaYubor("u1", "S", "M");
expect(tokenRepo.delete).toHaveBeenCalledWith({ token: In(["eskirgan-token"]) }); // 2.6
});
});Test qilish: ikki daraja. (1) Dry-run (
send(message, true)— ikkinchi argumentvalidateOnly) — FCM payload tuzilishini tekshiradi, lekin qurilmaga yetkazmaydi (CI/rivojlanishda xato payloadni topish uchun ideal). (2) Unit test —FirebaseServicevaRepositoryni mock qilib,messaging'ni soxta javob (bir yaxshi + bir eskirgan token) qaytaradigan qilib qo'yamiz va eskirgan token o'chirilishini 2.6-bob tekshiramiz — haqiqiy FCM'ga chiqmasdan. Firebase'ni DI orqali inyeksiya qilganimiz (2.3b) shu yerda foyda beradi — mock qo'yish oson. Qo'lda tekshirish uchun Firebase Console Cloud Messaging "Send test message" bilan bitta tokenga yuborib ko'ring.
5. To'g'ri va noto'g'ri holatlar
1) Eskirgan tokenni saqlab qolish
xato token DB'da qoladi (behuda urinish — 2.6)
yuborganda xato o'chirish2) Bir foydalanuvchi bitta token
faqat oxirgi qurilma (boshqalar push olmaydi — 2.4)
ko'p token (har qurilma)3) Ommaviy — har token alohida
million token bir-bir (sekin, limit — 2.9)
topic 2.8-bob yoki batch 500 + navbat4) Service account git'da
firebase JSON commit (to'lov kaliti! — 14)
.env + .gitignore5) Ortiqcha push (spam)
har mayda narsaga push (ilova o'chiriladi)
muhim + foydalanuvchi sozlamasi (2.10)6) data qiymatiga obyekt/son berish
data: { orderId: 123, ochiq: true } (FCM xato — faqat string!)
data: { orderId: "123", ochiq: "true" } (String(...) / JSON.stringify)7) Foreground push kutish (ilova ochiq)
ilova ochiq bo'lsa notification avtomatik chiqadi deb kutish
(Android/Web'da foreground'da tizim ko'rsatmaydi — 2.7a)
frontend onMessage handler'ida qo'lda ko'rsatish (local notification)8) Silent push'ni notification bilan yuborish
silent (fon sinxron) uchun notification qo'shish (ekranda chiqadi, foydalanuvchi bezovta)
faqat data + apns content-available:1 / android priority normal (2.7b)6. Keng tarqalgan xatolar va yechimlari
Xato 1 — Push kelmaydi
Sababi: token noto'g'ri/eskirgan, yoki frontend setup 2.4-bob. Yechimi: tokenni tekshiring; frontend service worker (web) sozlamasini tekshiring.
Xato 2 — privateKey xato (sozlash)
Sababi: \n almashtirilmagan 2.3-bob. Yechimi: .replace(/\\n/g, "\n").
Xato 3 — data xabar ko'rinmaydi
Sababi: data-only (background'da ko'rsatilmaydi — 2.7). Yechimi: notification + data birga.
Xato 4 — Token bazasi shishadi
Sababi: eskirgan tozalanmagan 2.6-bob. Yechimi: xato o'chirish.
Xato 5 — Ommaviy sekin/limit
Sababi: har token alohida 2.9-bob. Yechimi: topic / batch + navbat.
Xato 6 — data qiymat xato
Sababi: data — faqat string 2.7-bob. Yechimi: String(value) / JSON.stringify.
7. Integratsiya — bu mavzu stack'ning qayerida uchraydi
- SMS 5.18-bob, Email 8.10-bob: multi-channel.
- Navbat 8.22-bob: ommaviy push.
- Cron 8.21-bob: faolsiz tokenlarni davriy tozalash (2.8b).
- Real-time 8.18-bob: WebSocket (online) + push (offline).
- Config 8.14-bob: service account.
- Payment 8.19-bob: to'lov bildirishnomasi.
- Frontend (11): token olish, ko'rsatish (foreground/background handling — 2.7a).
- Xavfsizlik (14): service account, VAPID, token endpoint guard (2.10c).
- Strategy 9.2-bob: kanal.
- DI/Config (2.3b):
forRootAsync, ConfigService, testda mock. - Throttler 8.5-bob: push spam cheklash (rate limit — 2.9a).
- Alternativa: Web Push VAPID (2.10a), Expo push (2.10b) — FCM'siz muqobillar.
8. Eng yaxshi amaliyotlar (best practices)
- Token DB'da, bir user ko'p token (saqlash/yangilash — 2.4).
- Eskirgan token o'chirish (xato tozalash — 2.6).
- notification + data birga (ko'rsatish + deep link — 2.7).
- Topic (ommaviy) / batch 500 + navbat (2.8, 2.9).
- Service account .env (maxfiy — 14, 2.3).
- Foydalanuvchi sozlamasi (yoqish/o'chirish — 2.10).
- Multi-channel (push+SMS+email — muhimlikka — 2.10).
- Spam qilmang (muhim push); in-app tarix (Misol 9).
- data string (FCM cheklovi — 2.7).
- Promise.allSettled (kanal mustaqil — 8.10).
- Async DI modul (
FirebaseModule.forRootAsync— ConfigService, testda mock — 2.3b). - Platform config (
android/apns/webpush— har platforma o'z sozlamasi — 2.7b). - priority/TTL/collapse_key (high — kritik, normal — marketing; TTL — vaqtga bog'liq — 2.7c).
- Xato kodlarini klassifikatsiya (o'chir / qayta urin / tekshir — 2.9b).
- Rate limit (navbat rate limiter, backoff — FCM/APNs'ni ko'mmang — 2.9a).
- Dry-run + unit mock test (validateOnly, FirebaseService mock — Misol 11).
- Xavfsizlik (token endpoint JWT guard, payload'da maxfiy ma'lumot yo'q — 2.10c).
- Token rotation'ni kutib oling (
onTokenRefreshupsert;oxirgiIshlatilgan+ cron tozalash — 2.8b). conditionbilan segmentatsiya (region+til+kategoriya, max 5 topic — 2.8a).- To'g'ri API (
send— bitta;sendEachForMulticast— ko'p token;sendEach— turli xabar — 2.5).
9. Amaliy loyiha: "Push Bildirishnoma Tizimi"
Push'ni amalda mustahkamlash.
Maqsad
To'liq bildirishnoma tizimi: FCM push, token boshqaruvi, topic, ommaviy, multi-channel.
Talablar (requirements)
- Firebase setup: Admin SDK (Misol 1, 2.3).
- Token: saqlash/o'chirish, ko'p qurilma (Misol 2, 3, 2.4).
- Yuborish: bitta foydalanuvchi + eskirgan tozalash (Misol 3, 2.5, 2.6).
- Notification + data: deep link (Misol 5, 2.7).
- Topic: obuna + ommaviy (Misol 4, 2.8).
- Ommaviy: navbat + batch (Misol 6, 2.9).
- Multi-channel: push+SMS+email (Misol 7, 2.10).
- Sozlamalar: foydalanuvchi tanlovi (Misol 8, 2.10).
- In-app tarix: DB (Misol 9).
- Xavfsizlik: service account .env 2.11-bob.
Maslahatlar (hint)
- Ko'p token (2.4, 2-holat).
- Eskirgan tozalash (2.6, 4-xato).
- notification+data (2.7, 3-xato).
- Topic ommaviy (2.8, 3-holat).
- Service account .env (14, 4-holat).
- Spam qilmang (sozlama — 2.10).
"Tayyor" mezonlari (acceptance criteria)
- Firebase setup.
- Token (ko'p qurilma).
- Yuborish + tozalash.
- Deep link.
- Topic.
- Ommaviy (navbat).
- Multi-channel.
- Sozlamalar.
- In-app tarix.
- Xavfsizlik.
Yechim kodi ataylab berilmagan — bu loyihani o'zingiz yozib ko'ring.
10. Xulosa va keyingi bobga ko'prik
Bu bobda push bildirishnomani to'liq o'rgandik:
- Mexanizm (token oqimi — 2.1); FCM (platformalararo — 2.2); Admin SDK (sozlash — 2.3).
- Token boshqaruvi (ko'p qurilma, yangilash, eskirgan o'chirish — 2.4, 2.6); yuborish (bitta foydalanuvchi — 2.5).
- notification vs data (deep link — 2.7); topic (ommaviy — 2.8); navbat batch 2.9-bob; multi-channel (push+SMS+email — 2.10).
Keyingi bob — 8.24: Rasm/media qayta ishlash — sharp. Push'ni bildik; endi yana bir real mavzu — rasm qayta ishlash (resize, thumbnail, watermark, optimize, format) — ni o'rganamiz. Yuklangan rasmlarni avtomatik optimizatsiya (5.11 yuklashning davomi).
Foydalanilgan rasmiy/ishonchli manbalar
- firebase.google.com/docs/cloud-messaging (FCM, Admin SDK, topic, tokens)
- firebase.google.com/docs/reference/admin/node (firebase-admin
messaging()API —send,sendEachForMulticast,subscribeToTopic) - firebase.google.com/docs/cloud-messaging/send-message (notification/data payload,
android/apns/webpushconfig bloklari) - firebase.google.com/docs/cloud-messaging/manage-tokens (token boshqaruvi, rotation, eskirgan token tozalash)
- firebase.google.com/docs/cloud-messaging/send-message#send-messages-to-topics (topic va
condition— mantiqiy ifoda, max 5 topic) - developer.apple.com/documentation/usernotifications (APNs — iOS payload,
content-available) - github.com/web-push-libs/web-push (Web Push — VAPID kalitlar bilan, FCM'siz muqobil)
- docs.expo.dev/push-notifications/overview (Expo push — React Native muqobil)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!