SeekanDestroy
SeekanDestroy

Reputation: 611

AsyncLocalStorage not working for each request

I am using NestJS as a backend framework in NodeJS +16

I am trying to implement: https://medium.com/@sascha.wolff/advanced-nestjs-how-to-have-access-to-the-current-user-in-every-service-without-request-scope-2586665741f

My idea is to have a @Injectable() service that will have, among other things, methods like:

hasUserSomeStuff(){
const user = UserStorage.get()
if(user) {
// do magic
}

and then pass this service around as it is usually done in NestJS

To avoid passing the request down the rabbit hole, or bubbling up the request scope so every dependency gets instantiated for each request, but also avoiding to use UserStorage everywhere where I need to get the user from the current request and do stuff

I've gone through the docs many times, it is my understanding that node would take care of instantiating a new storage for each async context (in my case each request), but what seems to happen to me is that when I first run my backend, it works just fine, I've got the user from the current request, but once the first async context / promise is completed, I retrieved data for the consumer, and in the next request UserStorage returns a undefined (as doc states it will if you are outside of the same async context, which I guess it is not what happens, as it should be a brand new async context)

However if I debug, what seems to happen is that this UserStorage is called and a new AsyncLocalStorage is instantiated at init, before the app is ready to be used, and then the very first request returns a undefined user.

I am failing to understand what is going on, can anyone help me on this, or any better approach to achieve my goal?

Thanks in advance

Upvotes: 3

Views: 1692

Answers (1)

Paulo Santana
Paulo Santana

Reputation: 33

First we need to understand how AsyncLocalStorage works. You will be able to retrieve any information within the context that you explicitly execute from your AsyncLocalStorage instance.

for example:

//global scope 
const context = new AsyncLocalStorage()
//with typescript
//const context = new AsyncLocalStorage<UserPayload>()

async function test() {
  const userPayload = {
     id: 1,
     name: 'hiki9'
  }
  context.run(userPayload, () => whateverFunction()) 
}

async function whateverFunction() {
  const unknownPayload = context.getStore();
  //which is userPayload
}

Every function called after whateverFunction will share the context.

Assuming you are using NestJS, here is a common usage.

//author-context.service.ts
export interface AuthorContext {
  id: number
  name: string
}

@Injectable() 
export class AuthorUserContextService extends AsyncLocalStorage<AuthorContext>{}
//user.service.ts
@Injectable()
export class UserCreateService {
   constructor(private readonly context: AuthorUserContextService){}
   execute(dto: UserCreateDTO) {
      const store: AuthorContext = this.context.getStore()
      //const user = new User(store.whatever)
      //await this.userRepository.save(user)
      //inside userRepository, you can still `getStore`

   }
}

Then you need to AsyncLocalStorage.run at start of your execution thread. In the case you're doing some api with http, just bind it at begin of the Controller. In that case, using NestJS would be better configure a MiddlewareConsumer.

//iam.module.ts
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';

@Module({
  providers: [
    AuthorUserContextService,
    UserCreateService,
  ]
})
export class IamModule{
  constructor(
    //whateverServiceThatYouNeed: WhateverServiceThatYouNeed
  ) {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply((req: Request, res: Response, next: NextFunction) => {
          const userId = request.headers['user-internal-id'] //it will depends 
          const store = {
             userId,
             requestBody: request.body,
             //... 
          }
          this.context.run(store, () => next());
       })    
       .forRoutes('*');
  }
}

Upvotes: 1

Related Questions