|
| 1 | +### Async Local Storage |
| 2 | + |
| 3 | +`AsyncLocalStorage` is a [Node.js API](https://nodejs.org/api/async_context.html#async_context_class_asynclocalstorage) (based on the `async_hooks` API) that provides an alternative way of propagating state through the application without needing to explicitly pass it as a function parameter relying on REQUEST scoped providers and their limitations. It is similar to a thread-local storage in other languages. |
| 4 | + |
| 5 | +The main idea of AsyncLocalStorage is that we can _wrap_ some function call with the `AsyncLocalStorage#run` call. All code that is invoked within the wrapped call gets access to the same `store`, which will be unique to each call chain. |
| 6 | + |
| 7 | +In the context of NestJS, that means if we can find a place within the request's lifecycle, where we can wrap the rest of the request's code, we will be able to access and modify state visible only to that request. |
| 8 | + |
| 9 | +#### Custom implementation |
| 10 | + |
| 11 | +While NestJS itself does not provide a built-in support for it, there is a [3rd party package](#nestjs-cls) which supports many use-cases. However, let's first walk through how we could implement it ourselves for the simplest HTTP case, so we can get a better understanding of the whole concept: |
| 12 | + |
| 13 | +1. First, create a new instance of the `AsyncLocalStorage` in some shared source file. Since we're using NestJS, let's also turn it into a custom provider. |
| 14 | + |
| 15 | +```ts |
| 16 | +/** als.module.ts */ |
| 17 | + |
| 18 | +import { AsyncLocalStorage } from 'async_hooks'; |
| 19 | +import { Module } from '@nestjs/core'; |
| 20 | + |
| 21 | +export const asyncLocalStorage = new AsyncLocalStorage(); |
| 22 | + |
| 23 | +@Module({ |
| 24 | + providers: [ |
| 25 | + { |
| 26 | + provide: AsyncLocalStorage, |
| 27 | + useValue: asyncLocalStorage, |
| 28 | + }, |
| 29 | + ], |
| 30 | + exports: [AsyncLocalStorage], |
| 31 | +}) |
| 32 | +export class AlsModule {} |
| 33 | +``` |
| 34 | + |
| 35 | +2. Since we're only concerned with HTTP, let's use a middleware to wrap the `next` call with the `AsyncLocalStorage#run` call. This will make the `store` available in all enhancers and the rest of the system, since a middleware is the first thing that the request hits. |
| 36 | + |
| 37 | +```ts |
| 38 | +/** main.ts */ |
| 39 | + |
| 40 | +import { NestFactory } from '@nestjs/core'; |
| 41 | +import { AppModule } from './app.module'; |
| 42 | +import { asyncLocalStorage } from './als.setup.ts'; |
| 43 | + |
| 44 | +async function bootstrap() { |
| 45 | + const app = await NestFactory.create(AppModule); |
| 46 | + |
| 47 | + // Retrieve the instance from the container |
| 48 | + // (given we've imported the AlsModule in our AppModule). |
| 49 | + const als = app.get(AsyncLocalStorage); |
| 50 | + |
| 51 | + // Here we can bind the middleware to all routes, |
| 52 | + app.use((req, res, next) => { |
| 53 | + // populate the store with some default values, |
| 54 | + const store = { |
| 55 | + userId: req.headers['x-user-id'], |
| 56 | + }; |
| 57 | + // and and pass the "next" function as callback |
| 58 | + // to the "als.run" method with the default store. |
| 59 | + als.run(store, () => next()); |
| 60 | + }); |
| 61 | +} |
| 62 | + |
| 63 | +bootstrap(); |
| 64 | +``` |
| 65 | + |
| 66 | +3. Now, anywhere within the lifecycle of a request, we can access the local store instance |
| 67 | + |
| 68 | +```ts |
| 69 | +/** cat.service.ts */ |
| 70 | + |
| 71 | +export class CatService { |
| 72 | + constructor( |
| 73 | + private readonly als: AsyncLocalStorage, |
| 74 | + private readonly catRepository: CatRepository, |
| 75 | + ) {} |
| 76 | + |
| 77 | + getCatForUser() { |
| 78 | + // the "getStore" method will always return the |
| 79 | + // instance associated with the given request |
| 80 | + const userId = this.als.getStore().userId; |
| 81 | + return this.catRepository.getForUser(userId); |
| 82 | + } |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +4) That's it. Now you have a way to share request related state without needing to inject the whole `REQUEST` object. |
| 87 | + |
| 88 | +#### NestJS CLS |
| 89 | + |
| 90 | +The `nestjs-cls` package abstracts `AsyncLocalStorage` |
| 91 | + |
| 92 | +> info **info** `nestjs-cls` is a third party package and is not managed by the NestJS core team. Please, report any issues found with the library in the [appropriate repository](https://github.com/Papooch/nestjs-cls/issues). |
0 commit comments