Skip to content

Commit e28a635

Browse files
committed
docs(recipes): Add AsyncLocalStorage recipe page
1 parent a5b8181 commit e28a635

File tree

4 files changed

+109
-0
lines changed

4 files changed

+109
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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).

src/app/homepage/menu/menu.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export class MenuComponent implements OnInit {
240240
{ title: 'Prisma', path: '/recipes/prisma' },
241241
{ title: 'Serve static', path: '/recipes/serve-static' },
242242
{ title: 'Commander', path: '/recipes/nest-commander' },
243+
{ title: 'Async Local Storage', path: '/recipes/async-local-storage' },
243244
],
244245
},
245246
{
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
import { ChangeDetectionStrategy, Component } from '@angular/core';
3+
import { BasePageComponent } from '../../page/page.component';
4+
5+
@Component({
6+
selector: 'app-async-local-storage',
7+
templateUrl: './async-local-storage.component.html',
8+
changeDetection: ChangeDetectionStrategy.OnPush,
9+
})
10+
export class AsyncLocalStorageComponent extends BasePageComponent {}

src/app/homepage/pages/recipes/recipes.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { SqlTypeormComponent } from './sql-typeorm/sql-typeorm.component';
1616
import { TerminusComponent } from './terminus/terminus.component';
1717
import { RouterModuleComponent } from './router-module/router-module.component';
1818
import { NestCommanderComponent } from './nest-commander/nest-commander.component';
19+
import { AsyncLocalStorageComponent } from './async-local-storage/async-local-storage.component';
1920

2021
const routes: Routes = [
2122
{
@@ -95,6 +96,11 @@ const routes: Routes = [
9596
component: NestCommanderComponent,
9697
data: { title: 'Nest Commander' },
9798
},
99+
{
100+
path: 'async-local-storage',
101+
component: AsyncLocalStorageComponent,
102+
data: { title: 'Async Local Storage' },
103+
},
98104
{
99105
path: 'repl',
100106
component: ReplComponent,

0 commit comments

Comments
 (0)