Skip to main content

Command Palette

Search for a command to run...

Deep Dive NestJS Scoping

Updated
View as Markdown
Deep Dive NestJS Scoping

Untuk pengguna NestJS salah satu feature ini cukup berguna untuk dipahami, khususnya pengguna awal yang baru pindah sebagai contoh dari Express ke NestJS. Menurut kepercayaan saya, dengan memahami feature ini, programmer bisa lebih bijak dalam menggunakan memory. Lalu kenapa saya menggunakan istilah “berguna” dibanding “sangat perlu”. Sebab programmer masih bisa menggunakan alternative dengan merancang secara manual (merancang mandiri).

Module-module yang menggunakan decorator Injectable di NestJS secara default akan membuat singleton instance. Dimana ketika pertama kali sebuah module provider di-request untuk di-inject ke constructor module, NestJS akan membuat satu instance dari module tersebut. Lalu instance tersebutakan dibungkus oleh Module InstanceWrapper dan disimpan juga dibiarkan tetap ada di Heap selama lifecycle app masih dalam kondisi hidup. Sehingga ketika ada module yang perlu menggunakannya lagi, NestJS tidak akan membuat instance baru, tapi memberikan instance yang sama, yang sebelumnya sudah dibuat (Singleton).

Berikut adalah garis besar flow ketika pertama kali NestJS dijalankan.

Injection Scope Type NestJS

Pada NestJS terdapat tiga Scope Type yang bisa diimplementasikan.

DEFAULT (Singleton)

Setiap Module dengan Scope DEFAULT, akan dibuat menjadi instance ketika NestJS di-inisialisasi atau ketika pertama kali NestJS App dijalankan bersamaan pada saat NestJS menginisialisasi NestContainer.

// nest/packages/core/nest-factory.ts
export class NestFactoryStatic {
    public async create<T extends INestApplication = INestApplication>(
        moduleCls: IEntryNestModule,
        serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
        options?: NestApplicationOptions,
  ): Promise<T> {

     // ...

     const applicationConfig = new ApplicationConfig();
     const container = new NestContainer(applicationConfig, appOptions);
     const graphInspector = this.createGraphInspector(options!, container);

     // ...

     await this.initialize(
      moduleCls,
      container,
      graphInspector,
      applicationConfig,
      options,
    );

    // ...
    
  }

  private async initialize(
    module: any,
    container: NestContainer,
    graphInspector: GraphInspector,
    config = new ApplicationConfig(),
    options: NestApplicationContextOptions = {},
    httpServer: HttpServer | null = null,
  ) {

    // ...

    try {

      await ExceptionsZone.asyncRun(
        async () => {
          await dependenciesScanner.scan(module);
          await instanceLoader.createInstancesOfDependencies();
          dependenciesScanner.applyApplicationProviders();
        },
        teardown,
        this.autoFlushLogs,
      );
    } catch (e) {
      this.handleInitializationError(e);
    }
  }
}

Terlihat pada snippet code di atas, pada method initialize, module akan scanning semua module ( dependenciesScanner.scan ) lalu dilanjutkan untuk pembuatan instance tiap-tiap module yang memiliki attribute scope DEFAULT ( instanceLoader.createInstacesOfDependencies ). Jika tidak ada masalah lanjut pada tahap apply semua providers (dependenciesScanner.applyApplicationProviders).

Ketika ada module lain yang memerlukan module ini, semua module yang diimport akan reference ke instance yang sama atau tanpa membuat instance baru (Singleton). Instance-instance ini akan tetep berada di heap selama NestJS tetap hidup.

REQUEST

Pertama kali diinisialisasi, Nest App akan membuat instance NestContainer. Lalu Nest akan men-scan semua module-module yang terdaftar ( Seperti pada gambar flow di atas ). Setiap module yang ter-capture akan dibungkus (Wrapped) oleh InstanceWrapper. Berbeda dengan scope DEFAULT, NestJS tidak membuat instance pada saat initialisasi, tapi membuat instance pada saat request masuk.

Setiap kali request masuk ke presenter (ex: Controller) diterima oleh NestJS, NestJS akan mencari module mana yang memiliki Scope dengan tipe Scope.REQUEST . Jika ditemukan maka NestJS akan men-generate Random number sebagai Context ID dan membuat Instance setiap module yang ditemukan.

// nest/packages/core/injector/instance-wrapper.ts
export interface ContextId {
  readonly id: number;
  payload?: unknown;
  getParent?(info: HostComponentInfo): ContextId;
}

// nest/packages/core/helpers/context-id-factory.ts
export function createContextId(): ContextId {
  return { id: Math.random() };
}

Selanjutnya ContextID yang sudah dibuat digunakan sebagai flag/key yang akan di push ke InstanceWrapper values bertipe WeakMap dengan value instance dari module tersebut.

// nest/packagaes/core/injector/instance-wrapper.ts
export class InstanceWrapper<T = any> {
  public readonly name: any;
  public readonly token: InjectionToken;

  // ...

  private readonly values = new WeakMap<ContextId, InstancePerContext<T>>();

  // ...

}

Jadi setiap kali ada module yang melakukan permintaan untuk men-inject Module-module ini. Nest akan men-inject dari instance yang sudah dibuat ketika request masuk. Tidak membuat instance baru selama masih dalam satu lifecycle (request). Instance-instance ini tidak akan di Heap selamanya. Garbage Collector akan menghapusnya ketika sudah tidak ada yang menggunakan instance tersebut ( WeakMap ).

TRANSIENT

Module dengan jenis Scope TRANSIENT ini, sama seperti pada Scope DEFAULT . Akan dibuat menjadi instance ketika NestApp diinisialisasi setelah NestContainer dibuat. Hanya saja yang membedakannya adalah setiap kali module ini di-inject ke module lain yang membutuhkannya. NestJS akan membuat instance sendiri-sendiri untuk masing-masing module yang meng-import-nya. Instance-instance ini akan tetap di Heap selama app NestJS tetap hidup.

Kesimpulan

Scope Default dan Transient secara lifecycle terlihat mirip. Keduanya dibuat ketika Nest App menginisialisasi NestContainer. Keduanya juga tetap di Heap selama Nest App Hidup (Running). Yang membedakan diantaranya yaitu Scope DETAULT hanya membuat satu instance untuk berapapun module yang meng-import-nya. Sedangkan untuk Scope TRANSIENT akan dibuat instance Singleton sendiri-sendiri untuk tiap module yang meng-import-nya.

Untuk Scope REQUEST, instance akan dibuat ketika request datang dan instance akan sementara berada di Heap (WeakMap Type). Selanjutnya Garbage Collector yang akan membersihkannya dari Heap.

Dari bagaimana cara NestJS menyimpan Instance-instance tersebut di memory, programmer akan lebih bisa bijak bagaimana merancang architecture dari program-nya.

References