IlmHamroh
JavaScript Full-stack/30-qism. NestJS65/65-dars21 daqiqa
Mundarija (28)

WebSocket gateway'lar NestJS'da: @WebSocketGateway, SSE bilan o'lchov va yakuniy kanon

Qisqacha: Gateway — WebSocket uchun controller: @WebSocketGateway({ path }) klass, @SubscribeMessage("ping") metodlari, @MessageBody() va @ConnectedSocket() parametrlari. Ulanishning o'zi handleConnection(ws, req) da tekshiriladi — guard'lar faqat xabarlarga qo'llanadi, handshake'ga emas. WebSocket CORS'ga bo'ysunmaydi: begona sayt brauzer cookie'si bilan ulanadi (CSWSH), shuning uchun Origin qo'lda tekshiriladi. Adapter: WsAdapter (ws) yoki IoAdapter (Socket.IO). Kanon o'lchovi — 100 mijoz × 500 hodisa. Gateway 722 ms, 29-qismdagi SSE hub 173 ms, Nest @Sse 1 317 ms. vazifalar-api realtime uchun SSE'da qoldi (29-qism qarori: bir tomonlama oqim, cookie va HTTP infratuzilmasi bilan tabiiy).

Bu darsda

  • Nest gateway'ini lifecycle interfeyslari, xabar handler'lari va xato bilan to'liq yozasiz.
  • Handshake'da sessiya cookie'si va Origin ni tekshirasiz — haqiqiy chiqish bilan.
  • WsException, guard va adapterlarning (WsAdapter, IoAdapter, Redis adapter) o'rnini bilasiz.
  • Gateway, Nest @Sse va kanondagi SseHub o'lchovini o'qiysiz va sekin mijoz muammosini tushuntirasiz.
  • 29-qism integratsiyalarining Nest'ga ko'chishini yakuniy tekshiruv bilan tugatib, commit qilasiz.

Oldin bilishingiz kerak: WebSocket protokoli va ws kutubxonasi, Socket.IO, Realtime'ni masshtablash, Server-Sent Events NestJS'da, Global auth guard, @Public va @Roles, Guard'lar.

O'tgan darsda tashqi xizmatlar bitta modulga yig'ildi va SSE oqimi (/v1/oqim) ham shu modul ichida ulandi. Bugun realtime'ning ikkinchi yo'li — WebSocket — Nest'da qanday bo'lishini ko'ramiz. Keyin ikkalasini o'lchab, qism bo'yicha katta qadamni yakunlaymiz.

1. Nega bu kerak?

29-qismda realtime usullarini solishtirgandik (Realtime usullari va tanlov): polling, SSE, WebSocket. vazifalar-api uchun SSE tanlangan: server → mijoz bir tomonlama hodisalar ("vazifa o'zgardi"), cookie avtomatik ketadi, HTTP proxy va keshlar uni tushunadi.

WebSocket esa ikki tomonlama: chat, onlayn o'yin, birgalikda tahrirlash. Nest loyihalarida u juda keng tarqalgan — @nestjs/websockets paketi bilan. Ishga kirsangiz, gateway'ni ko'rasiz. Shuning uchun uni to'liq o'rganamiz, xavfsizlik tuzoqlari bilan.

O'xshatish: SSE — radio. Stansiya gapiradi, hamma eshitadi, eshituvchi javob bermaydi. WebSocket — telefon: ikki tomon ham gapiradi. Radio arzon va oddiy; telefon kerak bo'lsa — telefon.

2. Birinchi gateway

2.1 O'rnatish

bash
npm i @nestjs/websockets@12.2.0 @nestjs/platform-ws@12.2.0 ws

@nestjs/websockets — dekoratorlar va yadro. Adapter alohida: @nestjs/platform-ws (ws kutubxonasi, 29-qismdagi bilan bir xil — WebSocket protokoli va ws kutubxonasi) yoki @nestjs/platform-socket.io (Socket.IO). Biz ws bilan yozdik (biz ishga tushirdik: NestJS 12.2.0, ws 8.22.0, Node 24.21.0).

ts
const app = await NestFactory.create(M);
app.useWebSocketAdapter(new WsAdapter(app));
await app.listen(3000, "127.0.0.1");

useWebSocketAdapter — qaysi WebSocket kutubxonasi ishlatilishini aytadi. Gateway HTTP server bilan bitta portda ishlaydi: ulanish HTTP Upgrade so'rovi bilan boshlanadi.

2.2 Gateway klassi

Demo gateway — to'liq (qatorlar telefon uchun bo'lingan):

ts
const ISHONCHLI = new Set(["http://localhost:5173"]);
const egalar = new WeakMap<WebSocket, string>();

@WebSocketGateway({ path: "/v1/ws" })
class JonliGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer() server!: Server;

  afterInit(): void {
    log.push("afterInit: server tayyor");
  }

  handleConnection(ws: WebSocket, req: IncomingMessage): void {
    const origin = req.headers.origin;
    if (origin === undefined || !ISHONCHLI.has(origin)) {
      ws.close(4403, "origin");
      return;
    }
    const kim = /sessiya=(\w+)/.exec(req.headers.cookie ?? "")?.[1];
    if (kim === undefined) {
      ws.close(4401, "kirish-kerak");
      return;
    }
    egalar.set(ws, kim);
    log.push(`ulandi: ${kim}`);
  }

  handleDisconnect(ws: WebSocket): void {
    log.push(`uzildi: ${egalar.get(ws) ?? "noma'lum"}`);
  }

  @SubscribeMessage("ping")
  ping() {
    return { event: "pong", data: "ok" };
  }

  @SubscribeMessage("qosh")
  qosh(
    @MessageBody() tana: { matn?: string },
    @ConnectedSocket() ws: WebSocket,
  ) {
    if (!tana.matn) throw new WsException("matn bo'sh");
    return { event: "qoshildi", data: { matn: tana.matn,
      kim: egalar.get(ws) } };
  }

  @UseGuards(AdminGuard)
  @SubscribeMessage("tozala")
  tozala() {
    return { event: "tozalandi", data: true };
  }
}

Qismma-qism:

Element Vazifasi
@WebSocketGateway({ path }) gateway; ws adapterida yo'l, Socket.IO'da — namespace va cors
@WebSocketServer() adapterning server obyekti (ws da Server, hamma mijozlar server.clients)
afterInit (OnGatewayInit) server yaratilgach bir marta
handleConnection (OnGatewayConnection) har yangi ulanish — himoya joyi
handleDisconnect (OnGatewayDisconnect) ulanish yopildi
@SubscribeMessage("qosh") {"event":"qosh","data":…} xabari shu metodga
@MessageBody(), @ConnectedSocket() xabar tanasi va mijoz ulanishi
WsException xabar xatosi — mijozga exception hodisasi

Gateway — provider: modulning providers iga yoziladi (controller emas) va DI'dan xizmatlarni oladi. Bu demoda cookie qiymatining o'zi "foydalanuvchi" — haqiqiy sessiya tekshiruvi kanon tajribasida (quyida).

ws adapterida xabar formati — JSON { event, data }. Handler { event, data } qaytarsa, u shu mijozga yuboriladi. Hammaga yuborish — this.server.clients bo'ylab send.

2.3 Natija

Uchta ulanish va to'rtta xabar. O'qishdan oldin ikki narsani belgilang: rad etilgan ulanishlar HTTP status emas, yopish kodi oladi (4401, 4403), xabar xatolari esa ulanishni uzmaydi — exception hodisasi bo'lib keladi:

text
cookie'siz → 4401
begona Origin → 4403
ping → {"event":"pong","data":"ok"}
qosh → {"event":"qoshildi","data":{"matn":"Non","kim":"aziz"}}
qosh bo'sh → {"event":"exception","data":{"status":"error","message":"matn bo'sh","cause":{"pattern":"qosh","data":{}}}}
tozala (aziz) → {"event":"exception","data":{"status":"error","message":"Forbidden resource","cause":{"pattern":"tozala","data":{}}}}
afterInit: server tayyor
uzildi: noma'lum
uzildi: noma'lum
ulandi: aziz
uzildi: aziz

Qatorlarni o'qiymiz:

  • 4401, 4403 — WebSocket yopish kodlari. 4000–4999 — ilovaning o'z kodlari uchun ajratilgan; kanon HTTP 401 va 403 ga o'xshatib 4401/4403 ni tanlagan.
  • exception — WsException ni Nest'ning sukut WebSocket xato filtri shu shaklda yuboradi: status, message va cause — qaysi xabar (pattern) va qanday tana bilan kelgani. Ulanish uzilmaydi.
  • "Forbidden resource" — guard false qaytardi. HTTP'da bu 403 bo'lardi; WebSocket'da — exception hodisasi. Mijoz kodi buni alohida tutishi kerak.
  • "uzildi: noma'lum" ikki marta — rad etilgan ulanishlar uchun ham handleDisconnect chaqirildi. Biz close() qilganimizda ulanish allaqachon ochilgan edi. Shuning uchun handleDisconnect "bu ulanish ro'yxatda bormi" deb tekshirishi kerak — demoda egalar.get(ws) ?? "noma'lum".
  • Tartib — log oxirida chiqarildi, shuning uchun "ulandi: aziz" ikki rad etilgan uzilishdan keyin turibdi: ular avvalroq sodir bo'lgan.

HTTP so'rovining hayot sikli ni bilasiz. Gateway'da u ikkiga bo'linadi: ulanish bir marta, xabarlar esa ko'p marta keladi. Ikkalasining yo'li har xil:

Bosqich HTTP so'rovi WebSocket ulanishi WebSocket xabari
middleware ishlaydi yo'q (Upgrade Nest marshrutiga kirmaydi) yo'q
guard (himoyachi) ishlaydi yo'q — handleConnection qo'lda @UseGuards ishlaydi
interceptor, pipe ishlaydi yo'q ishlaydi
handler controller metodi handleConnection @SubscribeMessage metodi
xato filtri HTTP javob (401, 422…) — (o'zingiz ws.close(kod)) exception hodisasi, ulanish ochiq

Jadvaldagi eng muhim katak — "guard, ulanish" ustuni. HTTP'dagi barcha himoya (global KirishGuard, CheklovGuard) ulanish paytida ishlamaydi. Shuning uchun handleConnection — gateway'dagi yagona eshik: u yerda nima tekshirmasangiz, o'sha ochiq qoladi.

Tekshirib ko'ring: AdminGuard ni handleConnection ga qo'llab, "admin bo'lmaganni ulamaslik" mumkinmi?

Javob

Yo'q. Nest guard'lari, pipe va interceptor'lari gateway'da faqat @SubscribeMessage handler'lariga qo'llanadi. handleConnection — lifecycle metodi, u guard zanjiridan o'tmaydi. Ulanishni rad etish — handleConnection ichida qo'lda (ws.close(4403)), yoki adapter darajasida (verifyClient, Socket.IO'da middleware). Kanon tajribasi ham shuni tasdiqlaydi.

3. Xavfsizlik: CSWSH va handshake

3.1 Hujumchi nigohi

CSWSH (Cross-Site WebSocket Hijacking — saytlararo WebSocket o'g'irlash): foydalanuvchi vazifalar ilovasiga kirgan, brauzerda sessiya cookie'si bor. U boshqa saytni ochadi — yovuz.example. U saytdagi skript new WebSocket("wss://api.example.com/v1/ws") qiladi. Cookie SameSite=None bo'lsa yoki hujumchi sahifasi bir saytdagi boshqa subdomenda bo'lsa (same-site), brauzer cookie'ni o'zi qo'shadi. WebSocket uchun esa CORS yo'q — server javobini brauzer to'smaydi. Server Origin'ni tekshirmasa — begona sayt foydalanuvchi nomidan ulanib, uning vazifalari oqimini o'qiydi.

Himoya — handshake'da Origin sarlavhasini ruxsat ro'yxati (allowlist) bilan solishtirish. Brauzer Origin ni soxtalashtirishga yo'l qo'ymaydi. Demo natijadagi "begona Origin → 4403" — aynan shu himoya. Faqat o'z lokal loyihangizda sinang.

3.2 Kanon tajribasi: haqiqiy sessiya bilan

Kanon xuddi shunday gateway'ni 28-qismdagi haqiqiy sessiya ombori bilan yozdi (@nestjs/websockets 12.2.0, @nestjs/platform-ws 12.2.0). Handshake (qisqartirilgan):

ts
// Guard (@UseGuards) bu yerda ISHLAMAYDI — u faqat
// @SubscribeMessage handler'lariga; ulanish shu yerda tekshiriladi
async handleConnection(ws: WebSocket, req: IncomingMessage) {
  const origin = req.headers.origin;
  if (origin === undefined || !ISHONCHLI.has(origin)) {
    ws.close(4403, "origin");
    return;
  }
  const t = cookieOl(req.headers.cookie, cookieNomi(SUKUT));
  const s = t === undefined ? undefined : await sessiyalar.tekshir(t);
  if (s === undefined) {
    ws.close(4401, "kirish-kerak");
    return;
  }
  egalar.set(ws, s.foydalanuvchi.id);
}

sessiyalar.tekshir(token) — 28-qismdagi SessiyaOmbori: token hash'i bazada, muddati o'tmagan. Natija:

text
### 1-2. Handshake: cookie'siz, begona Origin (CSWSH), to'g'ri
cookie'siz                     → yopildi, kod 4401
begona Origin + cookie (CSWSH) → yopildi, kod 4403 (Origin tekshiruvisiz — ulanardi: brauzer cookie'ni yuboradi, CORS yo'q)
to'g'ri                        → ochiq; ping → {"event":"pong","data":"ok"}

Ikkinchi qatordagi izohga qarang: Origin tekshiruvisiz begona sayt haqiqiy cookie bilan ulanardi. Global KirishGuard bu yerda yordam bermaydi — u HTTP controller'lar uchun.

4. Adapterlar va masshtablash

Adapter Paket Xususiyat
WsAdapter @nestjs/platform-ws xom WebSocket, eng yengil; xona va qayta ulanish — o'zingiz
IoAdapter @nestjs/platform-socket.io Socket.IO: xonalar, namespace, ack, qayta ulanish, polling'ga tushish

Socket.IO'da @WebSocketGateway({ namespace: "/jonli", cors: { origin: [...] } }) va client.join("xona") bor (Socket.IO). Lekin Socket.IO mijozi ham Socket.IO bo'lishi shart — oddiy WebSocket u bilan gaplasha olmaydi.

Bir nechta server nusxasi. Mijoz A birinchi nusxaga, mijoz B ikkinchisiga ulangan. Birinchi nusxadagi hodisa B ga qanday yetadi? 29-qismda buni o'rgandik (Realtime'ni masshtablash): Redis Pub/Sub. Socket.IO uchun — @socket.io/redis-adapter: Nest'da IoAdapter dan meros olgan o'z adapter klassi yoziladi va createIOServer ichida Redis adapteri ulanadi. ws uchun — o'zingiz: har nusxa Redis kanalini tinglab, o'z mijozlariga yozadi. Kanondagi SseHub aynan shunday ishlaydi (SSE uchun). Redis adapter'ni biz sinamadik.

5. O'lchov: gateway, @Sse va SseHub

5.1 Uch yo'l, bitta yuk

Kanon bitta mashinada, bitta jarayonda uch realtime yo'lni bir xil yuk bilan o'lchadi: 100 mijoz, har biriga 500 hodisa — hammasi yetib kelguncha vaqt.

  • WebSocket gateway (ws) — shu darsdagi kabi.
  • Nest @Sse — Server-Sent Events NestJS'da darsidagi /v1/vazifalar/oqim: handler Observable<MessageEvent> qaytaradi, Nest javobni o'zi yozadi.
  • SseHub — 29-qismdagi SSE hub (@Res() bilan): Redis Stream tarixi, Last-Event-ID, sekin mijozni uzish.
text
### 1. 100 mijoz × 500 hodisa — hammasi yetib kelguncha (bir jarayon, lokal)
@Sse + Observable (A)     1317 ms, olindi: min 500 / 500
SseHub @Res (29 kanon)     173 ms, olindi: min 500 / 500
text
### 3. 100 mijoz × 500 hodisa (tC-sse bilan bir xil yuk)
WebSocket gateway (ws)  722 ms, hammasi 500 / 500
100 mijoz × 500 hodisa: hammasi yetib kelguncha (ms)
  • SseHub (@Res, 29-qism)173 ms
  • WebSocket gateway (ws)722 ms
  • Nest @Sse + Observable1 317 ms

Manba: Kanon o'lchovi (IlmHamroh, 2026-11-01): NestJS 12.2.0, @nestjs/platform-ws 12.2.0, ws 8.22.0, rxjs 7.8.2, bitta mashina, bitta jarayon

SseHub @Sse dan ~7.6× tez chiqdi. Sabab kanon tomonidan tekshirilmagan — taxmin: @Sse har hodisani RxJS zanjiri va Nest'ning SSE yozuvchisi orqali o'tkazadi, SseHub esa tayyor satrni to'g'ridan res.write qiladi. Gateway o'rtada. Hamma yo'lda 500/500 — hodisa yo'qolmadi. Bu o'lchovlar ikki alohida tajribada, lekin bir xil yukda va bir xil mashinada; nisbatga qarang.

5.2 Sarlavhalar

text
### 2. Javob sarlavhalari
/a: cache-control=private, no-cache, no-store, must-revalidate, max-age=0, no-transform; connection=keep-alive; content-type=text/event-stream; x-accel-buffering=no
/b: cache-control=no-cache, no-transform; connection=keep-alive; content-type=text/event-stream; charset=utf-8; x-accel-buffering=no

/a — @Sse, /b — SseHub. Ikkalasi ham x-accel-buffering: no qo'yadi — nginx oqimni buferlamasin (Reverse proxy orqasida Node). @Sse keshni qattiqroq o'chiradi.

5.3 Sekin mijoz

Eng muhim farq — o'qimaydigan mijoz. Telefon tunnelga kirdi: TCP ulanish ochiq, lekin ma'lumot o'qilmayapti. Server unga yozishda davom etsa, baytlar server xotirasida, ulanish buferida yig'iladi.

text
### 3. Uzilganda tozalash va sekin mijoz (o'qimaydi), 2 000 × 2 KB hodisa
@Sse obunalari (hamma mijoz uzilgandan keyin): 0; SseHub ulanishlari: 0
@Sse: obuna tirik (buferlaydi); SseHub: ulanishlar 0 (bufer 256 KB dan oshgach uzildi); heap farqi 13 MB

Birinchi qator: mijozlar uzilganda ikkalasi ham obunani to'g'ri tozaladi. Ikkinchi qator — farq: SseHub bitta sekin mijoz uchun bufer 256 KB dan oshishi bilan ulanishni uzdi (res.writableLength ni tekshiradi). @Sse esa yozishda davom etdi. Mijoz qayta ulanganda Last-Event-ID bilan o'tkazib yuborgan hodisalarni oladi — uzish xavfsiz.

Kichik model (brauzerda ishlaydi):

js
// Sekin mijoz modeli: hub har mijozga hodisa yozadi; o'qimaydigan
// mijozning buferi o'sadi. SseHub — chegaradan oshsa uzadi, @Sse esa
// buferlashda davom etadi (xotira o'sadi)
const KB = 1024;
function hubYarat({ maksBufer }) {
  const mijozlar = [];
  return {
    ulan(nom, oqiydi) {
      mijozlar.push({ nom, oqiydi, bufer: 0, ochiq: true });
    },
    yubor(bayt) {
      for (const m of mijozlar.filter((x) => x.ochiq)) {
        m.bufer = m.oqiydi ? 0 : m.bufer + bayt;
        if (maksBufer !== undefined && m.bufer > maksBufer) {
          m.ochiq = false; // uzildi
        }
      }
    },
    holat: () => mijozlar.map((m) =>
      `${m.nom}: ${m.ochiq ? "ochiq" : "uzildi"}, ` +
      `bufer ${Math.round(m.bufer / KB)} KB`),
  };
}

for (const [nom, sozlama] of [
  ["SseHub (256 KB)", { maksBufer: 256 * KB }],
  ["@Sse (chegarasiz)", {}],
]) {
  const hub = hubYarat(sozlama);
  hub.ulan("tez", true);
  hub.ulan("sekin", false);
  for (let i = 0; i < 2000; i++) hub.yubor(2 * KB);
  console.log(nom, "→", hub.holat().join(" | "));
}

Konsolda:

text
SseHub (256 KB) → tez: ochiq, bufer 0 KB | sekin: uzildi, bufer 258 KB
@Sse (chegarasiz) → tez: ochiq, bufer 0 KB | sekin: ochiq, bufer 4000 KB

Bitta sekin mijoz — 4 MB. Ming sekin mijoz — 4 GB. Model soddalashtirilgan: haqiqiy buferni Node oqimi va operatsion tizim boshqaradi, kanon tajribasida heap 13 MB o'sdi.

5.4 Kanon qarori

Mezon Gateway (ws) @Sse SseHub (kanon)
Yo'nalish ikki tomonlama server → mijoz server → mijoz
Auth qo'lda: cookie + Origin HTTP guard HTTP guard
Qayta ulanish va tarix o'zingiz yo'q Last-Event-ID + Redis Stream
Sekin mijoz o'zingiz buferlaydi 256 KB da uzadi
O'lchov (100 × 500) 722 ms 1 317 ms 173 ms

vazifalar-api ga ikki tomonlama kanal kerak emas — mijoz o'zgarishni REST orqali yuboradi, server esa hodisa bilan xabar beradi. SSE'da auth global KirishGuard dan o'tadi (HTTP so'rov), WebSocket'da esa himoyani qo'lda yozish kerak. @Sse esa sekin mijoz va tarix masalasida SseHub dan zaif. Shuning uchun:

  • /v1/oqim — SseHub (@Res() bilan), OqimController;
  • /v1/vazifalar/oqim — @Sse (oldingi qadamdan, bitta jarayon, EventEmitter) — qoldi, solishtirish uchun;
  • WebSocket gateway — kanonga kirmaydi.

Chat yoki birgalikda tahrirlash kerak bo'lsa — gateway to'g'ri tanlov, shu darsdagi Origin va handshake qoidalari bilan.

6. /v1/oqim — kanondagi controller

ts
@Controller("oqim")
@Cheklov(API_CHEKLOVI) // uzoq ulanish — bitta so'rov sifatida
export class OqimController {
  constructor(@Inject(JONLI) private readonly hub: SseHub) {}

  @Get()
  async ulan(
    @Egasi() egasi: number,
    @Req() req: Request,
    @Res() res: Response,
  ): Promise<void> {
    await this.hub.ulan(req, res, egasi);
  }
}

Uch qator ish: egasi (@Egasi(), guard o'tkazgan sessiyadan), so'rov va javob — hub'ga. Hammasi 29-qismdagi SseHub.ulan ichida: sarlavhalar, retry: 2000, Last-Event-ID dan tarix, faqat shu egasining hodisalari, heartbeat, sekin mijozni uzish. Kanon izohi nega @Sse() emasligini ham aytadi: "tarix → navbat → jonli" tartibi, takrorsiz yuborish, backpressure va Redis Pub/Sub tarqatuvchisi Response obyektini talab qiladi.

Jonli tekshiruvdan (Redis, ishchi relay bilan; 3 s tinglash, parallel POST):

text
$ curl -N /v1/oqim (3 s; parallel POST /v1/vazifalar, X-Jonli-Manba: curl-oyna)
retry: 2000

id: <stream-id>
event: ozgardi
data: {"manba":"curl-oyna","tur":"vazifa.yaratildi","vazifaId":51}

Hodisa yo'li: POST → bazaga vazifa + outbox hodisasi (bitta tranzaksiya) → ishchi relay → Redis Stream va Pub/Sub → API nusxasidagi SseHub → mijoz. id — Redis Stream id, har yugurishda boshqa (yashirilgan).

7. Kanon topgan xatolar — umumiy ro'yxat

29-qism integratsiyalari Nest'ga ko'chganda kanon topgan va tuzatgan xatolar (har biri o'z darsida batafsil):

Xato Qayerda Holati
Nest POST sukuti 201, @Res() bilan ham fayl tasdig'i (Fayl yuklash NestJS'da) @HttpCode(200) — tuzatildi
Parallel testlar bitta Redis Pub/Sub kanalida jonli/outbox testlari (Navbatlar) Vitest projects — tuzatildi
Vite (oxc) test ichidagi klass maydonlari — SyntaxError telegram-bot testi (Tashqi xizmatlar) klass modul darajasiga — tuzatildi
Obuna sahifasi formasi brauzerda yuborilmaydi (CSP) 29-qismdan (Email yetkazib berish) sahifaga alohida CSP — tuzatildi, brauzerda sinalmagan
curl -o /dev/null Windows'da exit 23 kanon tekshiruv skripti kut.mjs va -o NUL — tuzatildi
404 title inglizcha ("Not Found") fayl yo'llari, 29-qismdan tuzatilmagan (biz topdik)

Oxirgisidan oldingi qator — kodda emas, tekshiruv skriptida: Windows'dagi curl.exe MSYS yo'li /dev/null ga yoza olmaydi va 23 bilan chiqadi. "Server tayyormi" sikli shu sabab hech qachon to'xtamasdi. Kanon hisobotiga ko'ra, auth ko'chishi paytidagi "curl osilib qoldi" holatining asl sababi ham shu bo'lishi mumkin — bu tekshirilmagan.

Bundan tashqari kanon yangi test yozdi — test/nest-integratsiya.test.ts — 29-qism testlari ko'rmaydigan Nest'ga xos xavflar uchun: 22 ta integratsiya yo'lining hammasi anonimga 401, versiyasiz integratsiya yo'llari 404, Bull Board 403/200, obuna sahifasi CSP'si.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Gateway'ga hech kim ulanmaydi useWebSocketAdapter yo'q yoki mijoz Socket.IO/ws nomuvofiq adapter va mijoz bir xil kutubxona
@UseGuards ulanishni to'xtatmaydi guard faqat xabarlarga handleConnection da tekshiruv
Begona sayt ulanadi Origin tekshirilmaydi (CSWSH) ruxsat ro'yxati, close(4403)
Xato javobi exception hodisasi WsException / guard rad etdi mijozda exception ni tuting
Server xotirasi asta-sekin o'sadi sekin mijozlar buferlanadi bufer chegarasi va uzish
Ko'p nusxada hodisa yetmaydi har nusxa faqat o'z mijozlarini biladi Redis Pub/Sub yoki Redis adapter

9. Mashqlar

Mashqlar kurs/mashqlar/30/65-gateway/ da.

1-mashq (oson): to'ldiring

  • Xabar handler'i dekoratori: @("ping")
  • Ulanish tekshiriladigan lifecycle metodi:
  • ws adapterini ulash: app.(new WsAdapter(app))
  • Begona saytdan ulanishni to'sish uchun tekshiriladigan sarlavha:
Yechim
  • SubscribeMessage — { event: "ping" } xabari shu metodga.
  • handleConnection — OnGatewayConnection interfeysi; guard'lar bu yerda ishlamaydi.
  • useWebSocketAdapter — qaysi WebSocket kutubxonasi.
  • Origin — brauzer uni soxtalashtirishga yo'l qo'ymaydi; CSWSH'ga qarshi.

2-mashq (o'rta): handshake tekshiruvchisi

handshake(sarlavhalar, sessiyalar, ishonchli) — natija { ochiq: true, kim } yoki { ochiq: false, kod }. Avval Origin (yo'q yoki ro'yxatda emas — 4403), keyin sessiya=<token> cookie'si (yo'q yoki sessiyalar Map'ida yo'q — 4401). To'rt holatni sinang. Ishora: RegExp.exec (RegExp asoslari).

Yechim
js
function handshake(sarlavhalar, sessiyalar, ishonchli) {
  const origin = sarlavhalar.origin;
  if (origin === undefined || !ishonchli.has(origin)) {
    return { ochiq: false, kod: 4403 };
  }
  const token = /sessiya=(\w+)/.exec(sarlavhalar.cookie ?? "")?.[1];
  const kim = token === undefined ? undefined : sessiyalar.get(token);
  if (kim === undefined) return { ochiq: false, kod: 4401 };
  return { ochiq: true, kim };
}

const sessiyalar = new Map([["t0k3n", "aziz"]]);
const ishonchli = new Set(["http://localhost:5173"]);
const OK = "http://localhost:5173";
const holatlar = {
  "to'g'ri": { origin: OK, cookie: "sessiya=t0k3n" },
  "cookie'siz": { origin: OK },
  "eski token": { origin: OK, cookie: "sessiya=eski" },
  "CSWSH": {
    origin: "https://yovuz.example", cookie: "sessiya=t0k3n",
  },
};
for (const [nom, s] of Object.entries(holatlar)) {
  console.log(nom, "→", JSON.stringify(handshake(s, sessiyalar,
    ishonchli)));
}

Konsolda:

text
to'g'ri → {"ochiq":true,"kim":"aziz"}
cookie'siz → {"ochiq":false,"kod":4401}
eski token → {"ochiq":false,"kod":4401}
CSWSH → {"ochiq":false,"kod":4403}

Tartib muhim: Origin birinchi. CSWSH holatida cookie to'g'ri — tekshiruv cookie'dan boshlansa, begona sayt o'tib ketardi. Haqiqiy loyihada sessiyalar.get o'rniga 28-qismdagi sessiya ombori (bazadagi hash, muddat).

3-mashq (qiyin): egasi bo'yicha tarqatish

Paketsiz model. tarqat(mijozlar, egasi, hodisa) — faqat shu egasining ochiq mijozlariga hodisani yuborsin (m.qabul.push(hodisa)), yuborilganlar sonini qaytarsin. Uch mijoz: Aziz (2 ta ulanish, biri yopiq), Malika (1). Aziz uchun hodisa yuboring va har mijozning qabul qilganini chiqaring. Ishora: filter (filter).

Yechim
js
function tarqat(mijozlar, egasi, hodisa) {
  const kerak = mijozlar.filter((m) => m.egasi === egasi && m.ochiq);
  for (const m of kerak) m.qabul.push(hodisa);
  return kerak.length;
}

const mijozlar = [
  { nom: "Aziz telefon", egasi: 1, ochiq: true, qabul: [] },
  { nom: "Aziz noutbuk", egasi: 1, ochiq: false, qabul: [] },
  { nom: "Malika", egasi: 2, ochiq: true, qabul: [] },
];
const hodisa = { tur: "vazifa.yaratildi", vazifaId: 51 };
const n = tarqat(mijozlar, 1, hodisa);
console.log("yuborildi:", n);
for (const m of mijozlar) console.log(m.nom, "→", m.qabul.length);

Konsolda:

text
yuborildi: 1
Aziz telefon → 1
Aziz noutbuk → 0
Malika → 0

Malika Aziz'ning hodisasini olmadi — bu vazifalar-api ning asosiy qoidasi: har foydalanuvchi faqat o'z vazifalarini ko'radi. Kanon tajribasidagi gateway'da xuddi shu: egalar.get(k) === egasi && k.readyState === WebSocket.OPEN. Auth Nest'ga ko'chganda kanon xuddi shunday xavfni topgan: SSE hodisalari xotirada, RLS ularni himoya qilmaydi — shuning uchun har hodisaga egasi qo'shildi.

10. Loyiha qadami: integratsiyalar Nest'da — yakun va commit

Bu — 29-qism integratsiyalari ko'chishining oxirgi qismi. Avvalgi darslarda: fayllar, navbat va ishchi, IntegratsiyaModule va xizmat controller'lari. Bu darsda:

  1. src/realtime/oqim.controller.ts — «/v1/oqim» bo'limidagi OqimController (IntegratsiyaModule uni jonli yoqilganda qo'shadi).
  2. Yangi test — test/nest-integratsiya.test.ts (4 test): integratsiya yo'llari anonimga 401, versiyasiz integratsiya yo'llari 404, Bull Board 403/200, obuna sahifasi CSP'si.
  3. package.json: versiya 2.1.0, skriptlar ishchi va bot:
json
"ishchi": "node --env-file-if-exists=.env dist/ishchi.js",
"bot": "node --env-file-if-exists=.env dist/bot.js"
  1. API.md — auth va integratsiyalar bo'limlari (yangi yo'llar, statuslar).

WebSocket gateway qo'shilmaydi — «Kanon qarori» bo'limiga qarang. @nestjs/websockets o'rnatilmaydi.

10.1 Yakuniy tekshiruv

bash
npm run check
TESTCONTAINERS=1 npm test
npm run e2e

Kanon yakuniy tekshiruvi (Docker'dagi Postgres 18.6, Redis, MinIO, Mailpit; haqiqiy vazifalar_api, vazifalar_auth, vazifalar_ishchi rollari):

text
--- npm run tip
exit 0 (5940 ms)
--- npm run lint
exit 0 (12914 ms)
--- npm run test
 ↓ |asosiy| test/konteyner.test.ts (1 test | 1 skipped)
 Test Files  55 passed | 1 skipped (56)
      Tests  298 passed | 1 skipped (299)
exit 0 (82927 ms)
--- npm run e2e
 Test Files  3 passed (3)
      Tests  10 passed (10)
exit 0 (32280 ms)
--- npm run build
exit 0 (7254 ms)
--- Testcontainers (TESTCONTAINERS=1):  [Testcontainers vaqtlari] konteyner 2318 ms, migratsiya 3355 ms, snapshot 460 ms, restore (oxirgi) 192 ms  Tests 10 passed (10) ; kanon30-tc qoldiqlari: 0
Qadam Natija
tip / lint / build 0 / 0 / 0
Vitest (56 fayl, 2 loyiha) 298 o'tdi, 1 SKIP (konteyner — alohida 10/10)
e2e (Playwright, haqiqiy brauzer: CORS, CSRF, passkey) 10/10
Migratsiyalar 20 ta, schema.prisma bilan farq yo'q, sql/ bilan farq 0 qator
Jonli (uch rejim, API + ishchi) 200, 201, 202, 204, 206, 401, 403, 404, 409, 412, 422 — kutilgan; SSE hodisasi keldi; outbox — hammasi tarqatilgan

|asosiy| — Vitest loyihasi nomi (Navbatlar darsidagi projects). Testcontainers bu safar 10/10 — migratsiya 3.4 s.

Jonli tekshiruvda ilova ishga tushishi — 2.2–2.3 s: S3 bucket tayyorlash, SMTP verify() va Redis subscriber ulanishi bilan.

10.2 Commit

bash
git add -A
M="feat(api): 29-qism integratsiyalari NestJS'da — fayllar (MinIO),"
M+=" email, bildirishnomalar, Telegram, webhook obunalari, SSE,"
M+=" BullMQ va outbox (30-qism)"
git commit -m "$M"

git status ga qarang: .env, dist/, coverage/ kirmasin.

Bu commit nest tarmog'ida. Uni Nest CLI va loyiha tuzilmasi darsida ochgandingiz. Endi bu tarmoq main dagi Express API qila oladigan hamma narsani qiladi. Unda 28-qism auth'i ham, 29-qism integratsiyalari ham bor — xuddi o'sha testlar ostida. git log --oneline main..nest buyrug'i 30-qismda nest ga qo'shilgan hamma commit'ni ko'rsatadi: skeletdan shu commit'gacha butun ko'chish tarixi.

Shu bilan 30-qismning loyiha qismi tugadi. vazifalar-api 25–29-qismlardagi hamma narsa bilan — REST API, PostgreSQL va Prisma, Redis kesh, auth va xavfsizlik, fayllar, email, bildirishnomalar, Telegram, webhook'lar, SSE, navbat va outbox — NestJS 12 da ishlaydi. HTTP shartnomasi o'zgarmadi: 25–29-qismlarning testlari Nest'da o'tdi.

11. Real ishda

  • Gateway — Nest'dagi chat, bildirishnoma va "kim onlayn" funksiyalarining standart yo'li, ko'pincha Socket.IO bilan. Intervyuda "WebSocket'ni qanday himoya qilasiz?" savoliga to'g'ri javob — handshake'da auth va Origin, guard emas.
  • SSE vs WebSocket — ko'p loyihalar WebSocket'ni "zamonaviy" deb tanlaydi, lekin bir tomonlama hodisalar uchun SSE oddiyroq: HTTP auth, proxy, qayta ulanish brauzerda tayyor.
  • Sekin mijoz — prodakshndagi xotira oqishlarining tez-tez uchraydigan sababi. Har realtime server buferni cheklashi kerak.
  • Intervyu: "Nest gateway'da guard qayerda ishlaydi?", "CSWSH nima?", "Ko'p server nusxasida WebSocket qanday masshtablanadi?", "Qachon SSE, qachon WebSocket?".

Xulosa

  • Gateway — WebSocket uchun provider: @WebSocketGateway, @SubscribeMessage, @MessageBody, @ConnectedSocket, lifecycle interfeyslari; adapter — WsAdapter yoki IoAdapter.
  • Guard'lar faqat xabarlarga; ulanish handleConnection da cookie va Origin (CSWSH) bilan qo'lda tekshiriladi.
  • WsException va rad etgan guard mijozga exception hodisasi bo'lib boradi; handleDisconnect rad etilgan ulanish uchun ham chaqiriladi.
  • O'lchov (100 × 500): SseHub 173 ms, gateway 722 ms, @Sse 1 317 ms; @Sse sekin mijozni buferlaydi, SseHub 256 KB da uzadi.
  • Kanon: realtime — SSE (/v1/oqim, SseHub), gateway kanonga kirmaydi; integratsiyalar ko'chishi yakunlandi: Vitest 298/299, e2e 10/10, Testcontainers 10/10.

Keyingi dars: SQLite — 31-qism: prodakshn backend. Endi vazifalar-api tayyor; navbatda ma'lumotlar bazalarining boshqa turlari, replikatsiya va yuqori mavjudlik.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
WebSocket gateway'lar NestJS'da: @WebSocketGateway, SSE bilan o'lchov va yakuniy kanon — IlmHamroh