Retseptlar6 min read

Async Local Storage

AsyncLocalStorage Node.js API ( asynchooks API ga asoslangan) bo'lib, lokal holatni funksiyaga parametr sifatida aniq uzatmasdan butun ilova bo'ylab uzatishning muqobil usulini taq

AsyncLocalStorage Node.js API ( async_hooks API ga asoslangan) bo'lib, lokal holatni funksiyaga parametr sifatida aniq uzatmasdan butun ilova bo'ylab uzatishning muqobil usulini taqdim etadi. Bu boshqa tillardagi thread-local storage ga o'xshaydi.

Async Local Storage ning asosiy g'oyasi shuki, biz ba'zi funksiyalarni AsyncLocalStorage#run chaqirig'i bilan o'rab olamiz. O'ralgan chaqiriq ichida ishga tushadigan barcha kodlar bir xil store ga (har bir chaqiriq zanjiri uchun noyob) kirish imkoniga ega bo'ladi.

NestJS kontekstida bu shuni anglatadiki, agar so'rov hayotiy sikli davomida qolgan so'rov kodini o'rashimiz mumkin bo'lgan joyni topsak, faqat o'sha so'rovga ko'rinadigan holatga kirish va uni o'zgartira olamiz, bu esa REQUEST scope dagi providerlar va ularning ba'zi cheklovlariga muqobil bo'lishi mumkin.

Yoki ALS dan tizimning faqat bir qismi uchun (masalan, transaction obyekti) kontekstni, uni servislarda aniq uzatmasdan, tarqatish uchun foydalanishimiz mumkin, bu izolyatsiya va inkapsulyatsiyani oshirishi mumkin.

Maxsus implementatsiya

NestJS o'zida AsyncLocalStorage uchun hech qanday o'rnatilgan abstraksiyani taqdim etmaydi, shuning uchun butun konsepsiyani yaxshiroq tushunish uchun uni eng sodda HTTP holati uchun qanday o'zimiz implementatsiya qilishimizni ko'rib chiqamiz:

Info

Tayyor maxsus paket uchun pastroqda o'qing.

  1. Avval, umumiy manba faylida AsyncLocalStorage ning yangi nusxasini yarating. NestJS dan foydalanganimiz sababli, uni maxsus providerga ega modulga aylantiramiz.
TypeScript
als.module
1@Module({
2  providers: [
3    {
4      provide: AsyncLocalStorage,
5      useValue: new AsyncLocalStorage(),
6    },
7  ],
8  exports: [AsyncLocalStorage],
9})
10export class AlsModule {}
Hint

AsyncLocalStorageasync_hooks dan import qilinadi.

  1. Biz faqat HTTP bilan ishlaymiz, shuning uchun next funksiyasini AsyncLocalStorage#run bilan o'rash uchun middleware dan foydalanamiz. Middleware so'rov keladigan birinchi nuqta bo'lgani uchun, bu store ni barcha enhancerlar va qolgan tizimda mavjud qiladi.
TypeScript
app.module
1@Module({
2  imports: [AlsModule],
3  providers: [CatsService],
4  controllers: [CatsController],
5})
6export class AppModule implements NestModule {
7  constructor(
8    // inject the AsyncLocalStorage in the module constructor,
9    private readonly als: AsyncLocalStorage
10  ) {}
11
12  configure(consumer: MiddlewareConsumer) {
13    // bind the middleware,
14    consumer
15      .apply((req, res, next) => {
16        // populate the store with some default values
17        // based on the request,
18        const store = {
19          userId: req.headers['x-user-id'],
20        };
21        // and pass the "next" function as callback
22        // to the "als.run" method together with the store.
23        this.als.run(store, () => next());
24      })
25      .forRoutes('*path');
26  }
27}
  1. Endi so'rov hayotiy siklining istalgan joyida lokal store nusxasiga murojaat qilishimiz mumkin.
TypeScript
cats.service
1@Injectable()
2export class CatsService {
3  constructor(
4    // We can inject the provided ALS instance.
5    private readonly als: AsyncLocalStorage,
6    private readonly catsRepository: CatsRepository,
7  ) {}
8
9  getCatForUser() {
10    // The "getStore" method will always return the
11    // store instance associated with the given request.
12    const userId = this.als.getStore()["userId"] as number;
13    return this.catsRepository.getForUser(userId);
14  }
15}
  1. Tamom. Endi butun REQUEST obyektini inject qilmasdan so'rov bilan bog'liq holatni almashish usuliga egamiz.
Warning

Bu usul ko'plab use-case lar uchun foydali bo'lsa-da, tabiatan kod oqimini yashiradi (implitsit kontekst yaratadi), shuning uchun undan mas'uliyat bilan foydalaning va ayniqsa kontekstual "God objects" yaratishdan saqlaning.

NestJS CLS

nestjs-cls paketi oddiy AsyncLocalStorage ishlatishga nisbatan bir nechta DX yaxshilanishlarini taqdim etadi (CLS continuation-local storage atamasining qisqartmasi). U implementatsiyani ClsModule ga abstraksiyalaydi va store ni turli transportlar (faqat HTTP emas) uchun boshlashning turli usullarini hamda qat'iy tiplashni qo'llab-quvvatlaydi.

Store ga keyin injectable ClsService yordamida kirish mumkin, yoki uni Proxy Providers orqali biznes mantiqdan to'liq abstraksiyalash mumkin.

Info

nestjs-cls uchinchi tomon paketi bo'lib, NestJS core jamoasi tomonidan boshqarilmaydi. Kutubxona bilan bog'liq muammolarni tegishli repoda xabar bering.

O'rnatish

@nestjs kutubxonalariga peer qaramlikdan tashqari, u faqat o'rnatilgan Node.js API dan foydalanadi. Uni boshqa paketlar kabi o'rnating.

Terminal
1npm i nestjs-cls

Foydalanish

Yuqorida tasvirlangan o'xshash funksionallik ni nestjs-cls yordamida quyidagicha amalga oshirish mumkin:

  1. ClsModule ni ildiz modulga import qiling.
TypeScript
app.module
1@Module({
2  imports: [
3    // Register the ClsModule,
4    ClsModule.forRoot({
5      middleware: {
6        // automatically mount the
7        // ClsMiddleware for all routes
8        mount: true,
9        // and use the setup method to
10        // provide default store values.
11        setup: (cls, req) => {
12          cls.set('userId', req.headers['x-user-id']);
13        },
14      },
15    }),
16  ],
17  providers: [CatsService],
18  controllers: [CatsController],
19})
20export class AppModule {}
  1. So'ng ClsService ni store qiymatlariga kirish uchun ishlatish mumkin.
TypeScript
cats.service
1@Injectable()
2export class CatsService {
3  constructor(
4    // We can inject the provided ClsService instance,
5    private readonly cls: ClsService,
6    private readonly catsRepository: CatsRepository,
7  ) {}
8
9  getCatForUser() {
10    // and use the "get" method to retrieve any stored value.
11    const userId = this.cls.get('userId');
12    return this.catsRepository.getForUser(userId);
13  }
14}
  1. ClsService boshqaradigan store qiymatlarining qat'iy tiplanishini olish (va string kalitlar uchun auto-suggestion ga ega bo'lish) uchun uni inject qilganda ixtiyoriy ClsService<MyClsStore> tip parametridan foydalanishimiz mumkin.
TypeScript
1export interface MyClsStore extends ClsStore {
2  userId: number;
3}
Hint

Paketning so'rov ID sini avtomatik generatsiya qilishiga ruxsat berish va keyin cls.getId() orqali olish, yoki butun Request obyektini cls.get(CLS_REQ) orqali olish ham mumkin.

Testlash

ClsService shunchaki yana bir injectable provider bo'lgani uchun, uni unit testlarda to'liq mock qilish mumkin.

Ammo ayrim integratsion testlarda haqiqiy ClsService implementatsiyasidan foydalanishni xohlashimiz mumkin. Bunday holatda kontekstga bog'liq kod qismini ClsService#run yoki ClsService#runWith chaqirig'i bilan o'rashimiz kerak bo'ladi.

TypeScript
1describe('CatsService', () => {
2  let service: CatsService
3  let cls: ClsService
4  const mockCatsRepository = createMock<CatsRepository>()
5
6  beforeEach(async () => {
7    const module = await Test.createTestingModule({
8      // Set up most of the testing module as we normally would.
9      providers: [
10        CatsService,
11        {
12          provide: CatsRepository
13          useValue: mockCatsRepository
14        }
15      ],
16      imports: [
17        // Import the static version of ClsModule which only provides
18        // the ClsService, but does not set up the store in any way.
19        ClsModule
20      ],
21    }).compile()
22
23    service = module.get(CatsService)
24
25    // Also retrieve the ClsService for later use.
26    cls = module.get(ClsService)
27  })
28
29  describe('getCatForUser', () => {
30    it('retrieves cat based on user id', async () => {
31      const expectedUserId = 42
32      mocksCatsRepository.getForUser.mockImplementationOnce(
33        (id) => ({ userId: id })
34      )
35
36      // Wrap the test call in the `runWith` method
37      // in which we can pass hand-crafted store values.
38      const cat = await cls.runWith(
39        { userId: expectedUserId },
40        () => service.getCatForUser()
41      )
42
43      expect(cat.userId).toEqual(expectedUserId)
44    })
45  })
46})

Qo'shimcha ma'lumot

To'liq API hujjatlari va ko'proq kod namunalari uchun NestJS CLS GitHub sahifasi ga tashrif buyuring.