Async Local Storage
AsyncLocalStorage
est une API Node.js (basée sur l’API async_hooks
) qui fournit une alternative pour la propagation de l’état local à travers l’application sans avoir besoin de le passer explicitement en tant que paramètre de fonction. Cela ressemble à un stockage local par thread dans d’autres langages.
L’idée principale du stockage local asynchrone est que nous pouvons encapsuler un appel de fonction avec l’appel AsyncLocalStorage#run
. Tout code invoqué dans l’appel encapsulé a accès au même store
, qui sera unique pour chaque chaîne d’appels.
Dans le contexte de NestJS, cela signifie que si nous pouvons trouver un endroit dans le cycle de vie de la requête où nous pouvons encapsuler le reste du code de la requête, nous serons en mesure d’accéder et de modifier l’état visible uniquement pour cette requête, ce qui peut servir comme alternative aux fournisseurs à portée de REQUÊTE et à certaines de leurs limitations.
Alternativement, nous pouvons utiliser l’ALS pour propager le contexte uniquement pour une partie du système (par exemple, l’objet transaction) sans le transmettre explicitement entre les services, ce qui peut augmenter l’isolation et l’encapsulation.
Custom implementation
NestJS lui-même ne fournit aucune abstraction intégrée pour AsyncLocalStorage
, alors examinons comment nous pourrions l’implémenter nous-mêmes pour le cas HTTP le plus simple afin de mieux comprendre le concept global :
- Tout d’abord, créez une nouvelle instance de
AsyncLocalStorage
dans un fichier source partagé. Étant donné que nous utilisons NestJS, transformons-le également en un module avec un fournisseur personnalisé.
@Module({ providers: [ { provide: AsyncLocalStorage, useValue: new AsyncLocalStorage(), }, ], exports: [AsyncLocalStorage],})export class AlsModule {}
- Nous nous concentrons uniquement sur HTTP, alors utilisons un middleware pour encapsuler la fonction
next
avecAsyncLocalStorage#run
. Puisqu’un middleware est la première chose que la requête touche, cela rendra lestore
disponible dans tous les enhanceurs et le reste du système.
@Module({ imports: [AlsModule], providers: [CatService], controllers: [CatController],})export class AppModule implements NestModule { constructor(private readonly als: AsyncLocalStorage) {}
configure(consumer: MiddlewareConsumer) { consumer .apply((req, res, next) => { const store = { userId: req.headers['x-user-id'], }; this.als.run(store, () => next()); }) .forRoutes('*'); }}
- Maintenant, n’importe où dans le cycle de vie d’une requête, nous pouvons accéder à l’instance du magasin local.
@Injectable()export class CatService { constructor( private readonly als: AsyncLocalStorage, private readonly catRepository: CatRepository, ) {}
getCatForUser() { const userId = this.als.getStore()['userId'] as number; return this.catRepository.getForUser(userId); }}
- C’est tout. Maintenant, nous avons un moyen de partager l’état lié à la requête sans avoir besoin d’injecter l’objet
REQUEST
entier.
NestJS CLS
Le package nestjs-cls fournit plusieurs améliorations DX par rapport à l’utilisation du simple AsyncLocalStorage
(les initiales CLS signifient “continuation-local storage”). Il aborde l’implémentation dans un ClsModule
qui offre diverses façons d’initialiser le store
pour différents transports (pas seulement HTTP), ainsi qu’un support de typage fort.
Le magasin peut ensuite être accessible avec un injectable ClsService
, ou complètement abstrait de la logique métier en utilisant Proxy Providers.
Installation
Mis à part une dépendance peer sur les bibliothèques @nestjs
, il utilise uniquement l’API Node.js intégrée. Installez-le comme n’importe quel autre package.
npm i nestjs-cls
Usage
Une fonctionnalité similaire à celle décrite ci-dessus peut être mise en œuvre en utilisant nestjs-cls
comme suit :
- Importez le
ClsModule
dans le module racine.
@Module({ imports: [ ClsModule.forRoot({ middleware: { mount: true, setup: (cls, req) => { cls.set('userId', req.headers['x-user-id']); }, }, }), ], providers: [CatService], controllers: [CatController],})export class AppModule {}
- Et ensuite, vous pouvez utiliser le
ClsService
pour accéder aux valeurs du magasin.
@Injectable()export class CatService { constructor( private readonly cls: ClsService, private readonly catRepository: CatRepository, ) {}
getCatForUser() { const userId = this.cls.get('userId'); return this.catRepository.getForUser(userId); }}
- Pour obtenir un typage fort des valeurs du magasin gérées par le
ClsService
(et également recevoir des suggestions automatiques des clés de chaîne), vous pouvez utiliser un paramètre de type optionnelClsService<MyClsStore>
lors de l’injection.
export interface MyClsStore extends ClsStore { userId: number;}
Testing
Étant donné que le ClsService
est simplement un autre fournisseur injectable, il peut être entièrement simulé dans les tests unitaires.
Cependant, dans certains tests d’intégration, nous pourrions encore vouloir utiliser l’implémentation réelle de ClsService
. Dans ce cas, nous devrons encapsuler le code conscient du contexte avec un appel à ClsService#run
ou ClsService#runWith
.
describe('CatService', () => { let service: CatService; let cls: ClsService;
const mockCatRepository = createMock<CatRepository>();
beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ CatService, { provide: CatRepository, useValue: mockCatRepository, }, ], imports: [ClsModule], }).compile();
service = module.get(CatService); cls = module.get(ClsService); });
describe('getCatForUser', () => { it('retrieves cat based on user id', async () => { const expectedUserId = 42; mockCatRepository.getForUser.mockImplementationOnce(id => ({ userId: id }));
const cat = await cls.runWith({ userId: expectedUserId }, () => service.getCatForUser());
expect(cat.userId).toEqual(expectedUserId); }); });});
More information
Visitez la page GitHub de NestJS CLS pour la documentation API complète et plus d’exemples de code.