IlmHamroh
JavaScript Full-stack/30-qism. NestJS10/65-dars18 daqiqa
Mundarija (25)

NestJS provider scope'lari: DEFAULT, REQUEST, TRANSIENT va scope yuqishi

Qisqacha: Scope — provider nusxasi qancha yashashi. DEFAULT (sukut) — butun ilovaga bitta nusxa, ishga tushishda yaratiladi. REQUEST — har so'rovga yangi nusxa, so'rov tugagach tashlanadi. TRANSIENT — har iste'molchi klassga alohida nusxa. REQUEST scope yuqadi: unga tayangan har provider va controller ham har so'rovda qayta yaratiladi. Kanon o'lchovida bu taxminan 43 % sekinlashtirdi, shuning uchun vazifalar-api da hamma provider DEFAULT; so'rov konteksti AsyncLocalStorage bilan olinadi.

Bu darsda

  • Uch scope'ni va har birining nusxasi qachon yaratilishini bilasiz.
  • Scope yuqishini (bubbling) kanon tajribasining haqiqiy sanog'ida ko'rasiz.
  • @Inject(REQUEST) bilan joriy so'rovni olishni va uning narxini bilasiz.
  • TRANSIENT "har iste'molchiga" degani aslida nimani anglatishini tushuntirasiz.
  • Nega vazifalar-api da hamma provider singleton ekanini o'lchov bilan asoslaysiz.

Oldin bilishingiz kerak: Custom provider'lar, Provider'lar va service'lar, AsyncLocalStorage, Correlation ID va so'rov kuzatuvi.

Provider'lar darsida singleton'ning ikkinchi tomonini ko'rgandik: nusxa hamma so'rovlar uchun umumiy, unga so'rov holati yozilmaydi. Unda "har so'rovga o'z nusxasi" kerak bo'lsa-chi? Nest buning uchun scope'larni beradi. Bu dars — tajriba: kanon uchala scope'ni o'lchadi va loyihaga birortasini kiritmadi. Nega — dars oxirida.

1. Nega bu kerak?

Tasavvur qiling, har log qatoriga joriy foydalanuvchi ID'sini qo'shmoqchisiz. Singleton xizmatda buni maydonga yozish xato — ikki parallel so'rov bir-birining ID'sini ustidan yozadi. To'g'ri yo'llar ikkita:

  1. Xizmat har so'rovda yangidan yaratilsin va so'rovni constructor'da olsin. Bu — REQUEST scope.
  2. Xizmat bitta qolsin, joriy so'rov esa "yashirin kanal" orqali olinsin. Bu — 24-qismdagi AsyncLocalStorage; vazifalar-api da joriyLog(this.log) shunday ishlaydi.

Birinchi yo'l oddiy ko'rinadi. Lekin uning narxi bor va u kutilmagan joylarga "yuqadi". Shuni o'lchab ko'ramiz.

O'xshatish: mehmonxona. DEFAULT — bitta qabulxona, hamma mehmonga xizmat qiladi. REQUEST — har mehmonga o'z shaxsiy xizmatchisi; u mehmon ketgach ishdan bo'shaydi. TRANSIENT — har bo'limning (oshxona, kir yuvish) o'z yordamchisi. Shaxsiy xizmatchi qulay, lekin har mehmonga yangi odam yollash qimmat.

2. Uch scope

2.1 Jadval

Scope Nusxa Qachon yaratiladi
DEFAULT butun ilovaga bitta ishga tushishda
REQUEST har so'rovga bitta so'rov kelganda; so'rov tugagach tashlanadi
TRANSIENT har iste'molchi klassga bitta iste'molchi yaratilganda

Hech narsa yozmasangiz — DEFAULT: shu paytgacha yozgan hamma provider'ingiz singleton edi. Boshqa scope faqat ataylab yoziladi.

Yozilishi:

ts
@Injectable({ scope: Scope.REQUEST })
class SorovXizmati { /* … */ }

@Injectable({ scope: Scope.TRANSIENT })
class Yozuvchi { /* … */ }

Custom provider'da ham: { provide: X, useClass: Y, scope: Scope.REQUEST }. Controller'ga ham scope berish mumkin: @Controller({ path: "t", scope: Scope.REQUEST }).

2.2 @Inject(REQUEST)

REQUEST scope'li provider joriy Express so'rovini constructor'da ola oladi:

ts
import { REQUEST } from "@nestjs/core";

@Injectable({ scope: Scope.REQUEST })
class SorovniOladi {
  constructor(@Inject(REQUEST) readonly req: Request) {}
}

REQUEST — Nest'ning tayyor tokeni. Uni so'ragan provider avtomatik REQUEST scope'ga o'tadi — boshqa yo'l yo'q: so'rov har safar boshqa.

2.3 TRANSIENT qachon foydali

TRANSIENT'ning eng ko'p uchraydigan ishlatilishi — kontekstli logger. Har xizmat logga o'z nomini yozmoqchi: [VazifalarService] …, [EksportService] …. Logger singleton bo'lsa, nom bitta maydonda turadi va oxirgi yozgan xizmat hammanikini ustidan yozadi. TRANSIENT bo'lsa, har iste'molchi o'z nusxasini oladi va o'z nomini qo'yadi.

Demo loyihada tekshirdik (biz ishga tushirdik: NestJS 12.2.0, Node 24.21.0):

ts
@Injectable({ scope: Scope.TRANSIENT })
class KontekstLog {
  #kontekst = "?";
  kontekstQoy(nom: string): void { this.#kontekst = nom; }
  yoz(xabar: string): string {
    return `[${this.#kontekst}] ${xabar}`;
  }
}

@Injectable()
class VazifalarService {
  constructor(readonly log: KontekstLog) {
    log.kontekstQoy("VazifalarService");
  }
}
// EksportService ham xuddi shunday, o'z nomi bilan

Ikki xizmat bittadan qator yozdi, keyin ularning loggerlarini solishtirdik:

text
[VazifalarService] vazifa yaratildi
[EksportService] CSV tayyor
bir xil nusxami: false

Nima bo'ldi? Har xizmat o'z KontekstLog nusxasini oldi (false), shuning uchun nomlar aralashmadi. Muhimi — ikkala xizmat ham DEFAULT qoldi: TRANSIENT, REQUEST'dan farqli, yuqmaydi va so'rov paytida qayta yaratilmaydi. Nusxalar ishga tushishda bir marta yaratiladi (kanon tajribasining A variantida ham transient: 1). Shuning uchun uning narxi deyarli yo'q. Nest'ning o'z Logger klassi ham shu g'oyaga qurilgan: new Logger(VazifalarService.name) — har klassga o'z konteksti (NestJS Logger).

Express'da bu muammo qanday yechilgan edi? 25-qismda pino'ning log.child(...) metodi bilan har so'rovga sorovId bog'langan "bola" logger yaratilgan edi. Xuddi shu metod bilan har modulga ham o'z bolasini berish mumkin: log.child({ modul: "eksport" }). TRANSIENT — xuddi shu g'oyaning Nest konteyneridagi shakli: bolani siz emas, konteyner yaratadi.

Tekshirib ko'ring: KontekstLog ni TRANSIENT emas, oddiy DEFAULT qilsak, yuqoridagi ikki qator qanday chiqadi?

Javob

Ikkalasi ham [EksportService] … bo'ladi. Singleton'da nusxa bitta, #kontekst maydoni ham bitta. Avval VazifalarService unga o'z nomini yozadi, keyin EksportService ustidan yozadi. Kim oxirgi yaratilgan bo'lsa — hamma log o'shaning nomi bilan chiqadi. Bu — darsning boshidagi "singleton'ga holat yozish" xatosi, faqat log ko'rinishida.

3. Kanon tajribasi: kim necha marta yaratildi

3.1 Tajriba sharti

Kanon bitta ilovani ikki variantda qurdi: Xizmat avval DEFAULT, keyin REQUEST. Har klass constructor'ida hisoblagich oshadi. Kod qisqartirilgan:

ts
@Injectable({ scope: Scope.TRANSIENT })
class Yozuvchi {
  constructor() { yaratildi.transient++; }
}

@Injectable() class Soat { /* yaratildi.soat++ */ }

@Injectable({ scope })
class Xizmat {
  constructor(
    readonly soat: Soat,
    readonly y1: Yozuvchi,
    readonly y2: Yozuvchi,
  ) { yaratildi.xizmat++; }
  ol() {
    return {
      ok: this.soat.hozir(),
      birXilTransient: this.y1 === this.y2,
    };
  }
}

@Controller()
class C {
  constructor(private readonly x: Xizmat) {
    yaratildi.controller++;
  }
  @Get("x") ol() { return this.x.ol(); }
}

E'tibor bering: Xizmat Yozuvchi ni ikki marta so'raydi (y1, y2). Uchinchi variant (C) — alohida kichik ilova: @Inject(REQUEST) bilan so'rovni oladigan provider. Har variantga 3 so'rov yuborildi (kanon tajribasi, Vitest muhitida).

3.2 Natija

Har variantda ikki sanoq bor: ishga tushgan zahoti va 3 so'rovdan keyin. Ikkalasini solishtiring — qaysi son o'sdi, qaysi biri joyida qoldi:

text
A) DEFAULT (singleton) xizmat
   ishga tushganda yaratilgan: {"soat":1,"xizmat":1,"controller":1,"transient":1}; 3 so'rovdan keyin: {"soat":1,"xizmat":1,"controller":1,"transient":1}; javob: {"ok":1,"birXilTransient":true}
B) REQUEST xizmat (controller ham har so'rovda yaratiladi — bubbling)
   ishga tushganda yaratilgan: {"soat":1,"xizmat":0,"controller":0,"transient":0}; 3 so'rovdan keyin: {"soat":1,"xizmat":3,"controller":3,"transient":3}; javob: {"ok":1,"birXilTransient":true}
C) REQUEST + @Inject(REQUEST)
   ishga tushganda yaratilgan: {"soat":0,"xizmat":0,"controller":0,"transient":0}; 3 so'rovdan keyin: {"soat":0,"xizmat":0,"controller":0,"transient":0}; javob: {"url":"/r"}

Har variantni o'qiymiz.

A — DEFAULT. Hammasi ishga tushishda bir martadan yaratildi va 3 so'rovdan keyin ham 1. Singleton — kutilganidek.

B — REQUEST. Ishga tushishda faqat Soat (u DEFAULT). xizmat, controller, transient — 0: ular so'rovni kutyapti. 3 so'rovdan keyin — har biri 3. Eng muhim raqam — controller: 3. Controller'ga scope yozilmagan, u DEFAULT. Lekin u REQUEST scope'li xizmatga tayanadi — va o'zi ham har so'rovda yaratildi. Bu — scope yuqishi (scope bubbling): REQUEST scope bog'liqlik zanjiri bo'ylab yuqoriga ko'tariladi.

C — @Inject(REQUEST). Hisoblagichlar 0 — bu ilovada A/B klasslari yo'q (alohida modul). Javob {"url":"/r"} — provider joriy so'rovni oldi.

Va birXilTransient: true — A da ham, B da ham. Xizmat Yozuvchi ni ikki marta so'radi, lekin bitta nusxa oldi. TRANSIENT "har inject'ga yangi" emas, "har iste'molchi klassga yangi". Xizmat — bitta iste'molchi, ikki parametr unga bitta nusxa. Boshqa klass ham Yozuvchi ni so'rasa — o'shanga alohida.

flowchart BT
  R["Xizmat: REQUEST"] --> C["Controller: DEFAULT → REQUEST"]
  S["Soat: DEFAULT"] --> R
  T["Yozuvchi: TRANSIENT"] --> R
  C --> H["har so'rovda yangi nusxa"]

Diagrammani pastdan yuqoriga o'qing: REQUEST Xizmat dan controller'ga "yuqdi". Soat esa ta'sirlanmadi — u xizmatning bog'liqligi, iste'molchisi emas. Yuqish faqat yuqoriga.

3.3 Narxi

Kanon har variantni yuk ostida o'lchadi: autocannon alohida jarayonda, 10 ulanish, 5 aylana × 3 s, variantlar navbat bilan. Mashinada boshqa kanon agentlari parallel ishlagan — mutlaq raqamlar keng oraliqda, shuning uchun nisbat muhim. Grafikda DEFAULT = 100 %:

Provider scope: so'rov/s (DEFAULT = 100 %)
  • DEFAULT100 %
  • REQUESTcontroller ham har so'rovda57 %
  • REQUEST + @Inject(REQUEST)boshqa, kichikroq ilova73 %

Manba: IlmHamroh kanon o'lchovi, 2026-10-27: Node 24.21.0, @nestjs/core 12.2.0, autocannon 10 ulanish, mediana; DEFAULT 11 244, REQUEST 6 382, C 8 255 req/s; bitta mashina, bir vaqtda, mashina band

  • REQUEST — DEFAULT'ning 57 %, ya'ni taxminan 43 % sekin. p99 kechikish 3 ms dan 8 ms ga o'sdi.
  • C (73 %) bilan solishtirish to'liq adolatli emas: u boshqa, kichikroq ilova (bitta provider, Soat va Yozuvchi siz). Har so'rovda kamroq obyekt yaratiladi — shuning uchun B dan tezroq.

Sekinlik qayerdan? Har so'rovda Nest yangi DI "kontekst"ni quradi: zanjirdagi har REQUEST nusxa yaratiladi, bog'liqliklari yechiladi, so'rov tugagach chiqindi yig'uvchi ularni tozalaydi. Singleton'da bu ish bir marta, ishga tushishda.

Kichik hisob. B variantda har so'rov uchun uchta obyekt yaratildi: xizmat, controller va yozuvchi. Sekundiga 6 000 so'rovda bu sekundiga 18 000 ta yangi obyekt — ularning hammasi bir necha millisekunddan keyin chiqindiga aylanadi. DEFAULT variantda bu son nol: so'rov tayyor obyektlardan o'tadi va hech narsa qoldirmaydi. Haqiqiy loyihada zanjir uzunroq — vazifalar-api da controller, xizmat, ombor, log bor — demak farq ham kattaroq bo'lishi mumkin.

Kanon bitta o'lchov xatosini ham topdi. Birinchi urinishda autocannon server bilan bitta jarayonda ishlagan va DEFAULT atigi 2 445 req/s ko'rsatgan — 5 baravar kam. Yuk generatori server bilan bir event loop'ni bo'lishgan. Yuk o'lchovida generator doim alohida jarayonda bo'lsin.

Tekshirib ko'ring: AuthService REQUEST scope'li. Uni VazifalarService (DEFAULT) ishlatadi, uni esa VazifalarController va EksportService. Nechta klass har so'rovda qayta yaratiladi?

Javob

To'rttasi: AuthService, VazifalarService, VazifalarController va EksportService. REQUEST AuthService dan zanjir bo'ylab yuqoriga yuqadi: unga tayangan VazifalarService REQUEST bo'ladi, unga tayangan controller va EksportService ham. Bitta xizmatga yozilgan scope butun ilovaning yarmini sekinlashtirishi mumkin. EksportService dagi #ishlar Map'i esa endi har so'rovda bo'sh — eksport butunlay buziladi.

4. JS modelida scope va yuqish

Scope qoidalarini kichik modelda takrorlaymiz — brauzerda ishlaydi. haqiqiyScope yuqishni hisoblaydi, ol nusxani mos jadvaldan oladi:

js
const royxat = new Map(); // klass → { scope, kerak }
const yaratildi = {};
const qosh = (K, scope, kerak = []) =>
  royxat.set(K, { scope, kerak });

// scope "yuqadi": bog'liqlik REQUEST bo'lsa, iste'molchi ham REQUEST
function haqiqiyScope(K) {
  const { scope, kerak } = royxat.get(K);
  if (scope !== "DEFAULT") return scope;
  const yuqdi = kerak.some((B) => haqiqiyScope(B) === "REQUEST");
  return yuqdi ? "REQUEST" : "DEFAULT";
}

const singletonlar = new Map();
function ol(K, sorov, ota) {
  const s = haqiqiyScope(K);
  let jadval = s === "DEFAULT" ? singletonlar : sorov;
  let kalit = K;
  if (s === "TRANSIENT") {
    // har iste'molchi KLASSIGA alohida nusxa
    jadval = ota.jadval;
    kalit = `${K.name}@${ota.nom}`;
  }
  if (jadval.has(kalit)) return jadval.get(kalit);
  const args = royxat.get(K).kerak
    .map((B) => ol(B, sorov, { nom: K.name, jadval }));
  const n = new K(...args);
  yaratildi[K.name] = (yaratildi[K.name] ?? 0) + 1;
  jadval.set(kalit, n);
  return n;
}

class Soat {}
class Yozuvchi {}
class Xizmat {
  constructor(soat, y1, y2) { this.birXil = y1 === y2; }
}
class Controller {
  constructor(x) { this.x = x; }
}

for (const scope of ["DEFAULT", "REQUEST"]) {
  singletonlar.clear();
  for (const k of Object.keys(yaratildi)) delete yaratildi[k];
  qosh(Soat, "DEFAULT");
  qosh(Yozuvchi, "TRANSIENT");
  qosh(Xizmat, scope, [Soat, Yozuvchi, Yozuvchi]);
  qosh(Controller, "DEFAULT", [Xizmat]);
  let c;
  for (let i = 0; i < 3; i++) c = ol(Controller, new Map());
  console.log(`${scope}: ${JSON.stringify(yaratildi)}`);
  console.log(`  controller: ${haqiqiyScope(Controller)},` +
    ` birXil: ${c.x.birXil}`);
}

Konsolda:

text
DEFAULT: {"Soat":1,"Yozuvchi":1,"Xizmat":1,"Controller":1}
  controller: DEFAULT, birXil: true
REQUEST: {"Soat":1,"Yozuvchi":3,"Xizmat":3,"Controller":3}
  controller: REQUEST, birXil: true

Sonlar kanon tajribasidagi A va B bilan bir xil. Uch joyga qarang:

  • new Map() — har "so'rov" uchun yangi jadval. REQUEST nusxalar shu jadvalda yashaydi va so'rov bilan birga yo'qoladi.
  • haqiqiyScope(Controller) — REQUEST, garchi qosh(Controller, "DEFAULT", …) yozilgan bo'lsa ham. Yuqish rekursiya bilan hisoblanadi (rekursiya).
  • TRANSIENT kalit — Yozuvchi@Xizmat: iste'molchi klass nomi bilan. Shuning uchun y1 va y2 bitta nusxa.

Model soddalashtirilgan: haqiqiy Nest TRANSIENT'ni har iste'molchi nusxasiga bog'laydi, modul chegarasi va "durable" kontekstlar ham bor.

5. Express'da qanday bo'lardi va nima yashirin

25-qismda "har so'rovga yangi obyekt" kerak bo'lsa, uni handler ichida o'zingiz yaratardingiz:

js
r.get("/hisobot", (req, res) => {
  const hisobot = new HisobotYasovchi(req.sorovId, ombor);
  res.json(hisobot.yasa());
});

Bu yerda narx ko'rinib turibdi: new — handler ichida, bitta obyekt, bitta joyda. Kod o'qigan odam "har so'rovda yangi obyekt" ekanini darrov ko'radi.

Nest'da REQUEST scope bu ishni yashirin qiladi. Siz faqat bitta klassga scope: Scope.REQUEST yozasiz. Qolgani — unga tayangan har klass ham har so'rovda yaratilishi — hech qayerda yozilmagan. Kanon tajribasidagi controller'ning scope maydoni yo'q edi, lekin u 3 so'rovda 3 marta yaratildi. Yangi dasturchi controller fayliga qarab buni bila olmaydi.

Bu — freymvorkning umumiy savdosi: kam kod evaziga kam ko'rinish. DI konteyner ulash ishini o'z zimmasiga olgani yaxshi; lekin u nusxa sonini ham yashirincha o'zgartira olishini yodda tuting.

5.1 Scope va lifecycle hook'lar

Yana bir yashirin farq: REQUEST scope'li klasslarda onModuleInit kabi hayot sikli ilgaklari (lifecycle hook'lar) chaqirilmaydi. Nest hujjati shunday deydi, biz demo loyihada tekshirdik. Ikki xizmatga onModuleInit yozdik: biri DEFAULT, ikkinchisi REQUEST. Ilovani ko'tarib, ikkita so'rov yubordik va qaysi ilgak chaqirilganini sanadik:

text
ishga tushgach onModuleInit: ["DEFAULT"]
2 so'rovdan keyin onModuleInit: ["DEFAULT"]

REQUEST xizmat har so'rovda yaratildi (kanon sanog'idagi B variant kabi), lekin uning onModuleInit i bir marta ham chaqirilmadi. Sabab: bunday klasslar ishga tushishda emas, so'rov paytida yaratiladi — "modul tayyor bo'ldi" degan lahzada ular hali yo'q. Agar xizmatga REQUEST yuqsa va uning onModuleInit i baza ulanishini ochayotgan bo'lsa — ulanish ochilmay qoladi, xato esa keyinroq, boshqa joyda chiqadi. Hook'larning o'zi — Lifecycle hook'lar darsida.

5.2 Scope'ni qanday topish mumkin

Katta loyihada "qaysi provider'lar REQUEST" degan savolga javob kerak bo'ladi. Uch yo'l:

  1. Qidiruv: kodda Scope.REQUEST, Scope.TRANSIENT va REQUEST tokenini qidirish. Bu ataylab yozilganlarni topadi, lekin yuqqanlarni emas.
  2. Hisoblagich: shubhali klass constructor'iga vaqtincha log qo'yish. Kanon tajribasi aynan shunday qilgan — "3 so'rovda 3 marta".
  3. Discovery: Nest'ning DiscoveryService i hamma provider'lar ro'yxatini va ularning isDependencyTreeStatic() belgisini beradi — "bog'liqlik daraxti statikmi", ya'ni yuqish bormi. Buni Reflector va DiscoveryService darsida ishlatamiz.

6. Durable provider'lar — qisqacha

REQUEST scope'ning narxini kamaytirish uchun Nest durable (bardoshli) provider'larni taklif qiladi. G'oya: so'rovlarni biror belgi bo'yicha guruhlash (masalan, mijoz kompaniyasi — tenant) va har guruhga bitta DI kontekst. Bir xil tenant'ning so'rovlari bitta nusxani bo'lishadi. Buning uchun ContextIdStrategy yoziladi va provider'ga durable: true beriladi.

Bu ko'p mijozli (multi-tenant) tizimlarda foydali: har mijozning o'z baza ulanishi, o'z sozlamasi. Kanon durable provider'larni sinamagan — yuqoridagilar Nest hujjati bo'yicha (2026-11 holatiga). vazifalar-api bitta egali — bunday ehtiyoj yo'q.

7. Nega vazifalar-api da hamma provider DEFAULT

Kanon qarori: scope o'zgartirilmaydi. Uch sabab:

  1. Narx. REQUEST ~43 % sekin (kanon o'lchovi), p99 2,5 baravar. Yuqish tufayli bitta xizmat butun zanjirni sekinlashtiradi.
  2. Holatli singleton'lar. EksportService ning #ishlar Map'i so'rovlar orasida yashashi shart. Unga REQUEST yuqsa — eksport buziladi.
  3. Muqobil bor. Joriy so'rov konteksti 24–25-qismdan beri AsyncLocalStorage orqali olinadi: sorovLogi middleware'i kontekstni ochadi, joriyLog(log) uni o'qiydi. Singleton xizmat har chaqiruvda joriy so'rov logger'ini oladi — yangi nusxa yaratmasdan. Buni Nest'ga moslashtirilgan kutubxona bilan qilish — So'rov konteksti: nestjs-cls darsida.

7.1 Joriy so'rov ma'lumotini olishning uch yo'li

"Xizmatga joriy foydalanuvchi yoki sorovId kerak" — bu vazifaning uch yechimi bor. Ularni yonma-yon qo'yamiz:

Yo'l Qanday Narxi
parametr controller @Req() dan olib, metodga uzatadi yo'q, lekin har metod imzosida qo'shimcha argument
AsyncLocalStorage middleware kontekst ochadi, xizmat o'qiydi juda kichik; singleton qoladi
REQUEST scope @Inject(REQUEST) constructor'da ~43 % sekin, yuqadi, ilgaklar ishlamaydi

Birinchi yo'l eng ochiq: kodni o'qigan odam ma'lumot qayerdan kelganini ko'radi. Lekin chaqiruv zanjiri uzun bo'lsa (controller → xizmat → ombor → log), argument har qavatdan o'tishi kerak. Ikkinchi yo'l shu muammoni yechadi va vazifalar-api aynan uni tanlagan. Uchinchisi Nest hujjatidagi eng sodda ko'rinadigan yo'l, lekin narxi eng katta.

REQUEST qachon to'g'ri tanlov? Kam so'rovli, lekin har so'rovda tubdan boshqa resurs kerak bo'lganda (tenant bo'yicha boshqa baza) — va durable bilan. Yoki tashqi kutubxona aynan REQUEST scope talab qilsa (masalan, ba'zi GraphQL dataloader naqshlari).

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Ilova sekinlashdi, sabab ko'rinmaydi bitta REQUEST provider butun zanjirga yuqdi scope'ni olib tashlang yoki AsyncLocalStorage
Singleton'dagi Map har so'rovda bo'sh xizmatga REQUEST yuqdi holatli xizmatni REQUEST zanjiridan ajrating
ModuleRef.get() xato beradi REQUEST/TRANSIENT provider get bilan olinmaydi resolve() (Circular dependency, ModuleRef)
TRANSIENT ikki parametrda bir xil nusxa "har iste'molchi klassga", "har parametrga" emas ikki xil token yoki fabrika
onModuleInit ishlamadi, ulanish ochilmadi xizmat REQUEST (yoki unga yuqqan) resursni DEFAULT provider'ga ko'chiring
Hamma log bitta xizmat nomi bilan kontekstli logger singleton TRANSIENT logger yoki log.child
Yuk o'lchovi juda past autocannon server bilan bitta jarayonda generatorni alohida jarayonda yurgizing

9. Mashqlar

Mashqlar kurs/mashqlar/30/10-scope/ da.

1-mashq (oson): scope'ni toping

  • Butun ilovaga bitta, ishga tushishda yaratiladi — Scope.
  • Har so'rovga yangi nusxa — Scope.
  • @Inject(REQUEST) ishlatgan provider avtomatik qaysi scope'da? —
  • Xizmat Yozuvchi (TRANSIENT) ni ikki parametrda so'radi. Nechta nusxa oladi? —
  • Kanon o'lchovida REQUEST DEFAULT'dan taxminan necha foiz sekin? —
Yechim
  • DEFAULT — sukut, singleton.
  • REQUEST — so'rov tugagach tashlanadi.
  • REQUEST — joriy so'rov har safar boshqa, singleton bo'la olmaydi.
  • 1 — TRANSIENT har iste'molchi klassga; kanonda birXilTransient: true.
  • 43 — 6 382 / 11 244 ≈ 0.57.

2-mashq (o'rta): yuqishni hisoblang

yuqishXaritasi(graf, scopelar) funksiyasini yozing. graf — { Klass: [bog'liqliklar] }, scopelar — faqat ataylab berilgan scope'lar ({ Auth: "REQUEST" }). Har klass uchun haqiqiy scope'ni qaytarsin. Sinov grafi: Controller → Xizmat → [Auth, Soat], Eksport → [Xizmat], Soat → [], Auth → []. Avval scope'siz, keyin Auth: "REQUEST" bilan. Ishora: rekursiya va some.

Yechim
js
function yuqishXaritasi(graf, scopelar) {
  const natija = {};
  function scope(k) {
    if (natija[k]) return natija[k];
    const oz = scopelar[k] ?? "DEFAULT";
    const yuqdi = graf[k].some((b) => scope(b) === "REQUEST");
    natija[k] = oz === "DEFAULT" && yuqdi ? "REQUEST" : oz;
    return natija[k];
  }
  return Object.fromEntries(
    Object.keys(graf).map((k) => [k, scope(k)]),
  );
}

const graf = {
  Controller: ["Xizmat"],
  Eksport: ["Xizmat"],
  Xizmat: ["Auth", "Soat"],
  Auth: [],
  Soat: [],
};
console.log(yuqishXaritasi(graf, {}));
console.log(yuqishXaritasi(graf, { Auth: "REQUEST" }));

Konsolda:

text
{
  Controller: 'DEFAULT',
  Eksport: 'DEFAULT',
  Xizmat: 'DEFAULT',
  Auth: 'DEFAULT',
  Soat: 'DEFAULT'
}
{
  Controller: 'REQUEST',
  Eksport: 'REQUEST',
  Xizmat: 'REQUEST',
  Auth: 'REQUEST',
  Soat: 'DEFAULT'
}

Bitta Auth: "REQUEST" yozuvi beshtadan to'rtta klassni REQUEST qildi. Soat qoldi — u faqat bog'liqlik. Katta loyihada bunday xaritani DiscoveryService bilan haqiqiy provider'lardan qurish mumkin.

3-mashq (qiyin): TRANSIENT — klass va nusxa

Darsdagi modelda TRANSIENT kalit iste'molchi klass nomi bo'yicha. Ikki xil klass (Hisobot, Eksport) Yozuvchi ni so'rasa, nechta Yozuvchi yaratiladi? Modelni ishlatib tekshiring: ikkala klass DEFAULT, har biri Yozuvchi ni bir marta so'raydi, bitta Ilova klassi ikkalasiga tayanadi. Keyin Hisobot ni REQUEST qilib, 2 so'rov uchun sanang. Ishora: darsdagi qosh/ol ni o'zgarishsiz oling.

Yechim
js
const royxat = new Map();
const yaratildi = {};
const qosh = (K, scope, kerak = []) =>
  royxat.set(K, { scope, kerak });
function haqiqiyScope(K) {
  const { scope, kerak } = royxat.get(K);
  if (scope !== "DEFAULT") return scope;
  const yuqdi = kerak.some((B) => haqiqiyScope(B) === "REQUEST");
  return yuqdi ? "REQUEST" : "DEFAULT";
}
const singletonlar = new Map();
function ol(K, sorov, ota) {
  const s = haqiqiyScope(K);
  let jadval = s === "DEFAULT" ? singletonlar : sorov;
  let kalit = K;
  if (s === "TRANSIENT") {
    jadval = ota.jadval;
    kalit = `${K.name}@${ota.nom}`;
  }
  if (jadval.has(kalit)) return jadval.get(kalit);
  const args = royxat.get(K).kerak
    .map((B) => ol(B, sorov, { nom: K.name, jadval }));
  const n = new K(...args);
  yaratildi[K.name] = (yaratildi[K.name] ?? 0) + 1;
  jadval.set(kalit, n);
  return n;
}

class Yozuvchi {}
class Hisobot { constructor(y) { this.y = y; } }
class Eksport { constructor(y) { this.y = y; } }
class Ilova { constructor(h, e) { this.h = h; this.e = e; } }

for (const hScope of ["DEFAULT", "REQUEST"]) {
  singletonlar.clear();
  for (const k of Object.keys(yaratildi)) delete yaratildi[k];
  qosh(Yozuvchi, "TRANSIENT");
  qosh(Hisobot, hScope, [Yozuvchi]);
  qosh(Eksport, "DEFAULT", [Yozuvchi]);
  qosh(Ilova, "DEFAULT", [Hisobot, Eksport]);
  let a;
  for (let i = 0; i < 2; i++) a = ol(Ilova, new Map());
  console.log(`Hisobot ${hScope}: ${JSON.stringify(yaratildi)}`,
    `bir xil: ${a.h.y === a.e.y}`);
}

Konsolda:

text
Hisobot DEFAULT: {"Yozuvchi":2,"Hisobot":1,"Eksport":1,"Ilova":1} bir xil: false
Hisobot REQUEST: {"Yozuvchi":3,"Hisobot":2,"Eksport":1,"Ilova":2} bir xil: false

Birinchi qator: ikki iste'molchi klass — ikki Yozuvchi, ular har xil (false). Ikkinchisi: Hisobot REQUEST bo'ldi — u har so'rovda yangi, demak uning Yozuvchi si ham har so'rovda yangi (2 so'rov → 2), Eksport niki esa bitta. Jami 3. Ilova ham 2 — unga Hisobot dan yuqdi.

10. Real ishda

  • Ko'p mijozli SaaS (har kompaniyaning o'z bazasi) — REQUEST scope va durable provider'lar uchrashadigan asosiy joy.
  • Kod review'da scope: Scope.REQUEST ko'rinsa, birinchi savol: "zanjirda kim bor va AsyncLocalStorage yetmaydimi?".
  • Loglarda TRANSIENT eng ko'p uchraydi: kutubxonalar (masalan, nestjs-pino'ning PinoLogger i) har xizmatga o'z kontekstli logger'ini shu yo'l bilan beradi. Uni ko'rganda qo'rqmang — TRANSIENT REQUEST kabi qimmat emas.
  • Profiling'da Nest ilovasi kutilmaganda sekin bo'lsa, scope'li provider'larni qidirish — tez tekshiriladigan sabablardan biri.
  • Intervyu: "Nest'da qanday scope'lar bor?", "Scope bubbling nima?", "Joriy foydalanuvchini singleton xizmatda qanday olasiz?" (AsyncLocalStorage, nestjs-cls yoki parametr).

Xulosa

  • DEFAULT — bitta nusxa (sukut); REQUEST — har so'rovga; TRANSIENT — har iste'molchi klassga.
  • REQUEST zanjir bo'ylab yuqoriga yuqadi: unga tayangan controller va xizmatlar ham har so'rovda yaratiladi.
  • @Inject(REQUEST) — joriy so'rov; uni so'ragan provider REQUEST bo'ladi.
  • TRANSIENT yuqmaydi va ishga tushishda yaratiladi — kontekstli logger uchun qulay; REQUEST klasslarda onModuleInit chaqirilmaydi.
  • Kanon o'lchovida REQUEST ~43 % sekin, p99 3 → 8 ms; o'lchovda yuk generatori alohida jarayonda bo'lsin.
  • vazifalar-api da hammasi DEFAULT: holatli singleton'lar va AsyncLocalStorage muqobili; durable — sinalmagan.

Keyingi dars: Circular dependency, ModuleRef va lazy modullar — aylanma bog'liqlik, forwardRef, ModuleRef.get/resolve/create va ESM'da testda o'tib, build'da yiqiladigan tuzoq.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
NestJS provider scope'lari: DEFAULT, REQUEST, TRANSIENT va scope yuqishi — IlmHamroh