npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@m16khb/nestjs-async-utils

v1.0.5

Published

NestJS 비동기 작업 제어 라이브러리 - @Retryable, @Timeout, @ConcurrencyLimit 데코레이터

Downloads

617

Readme

@m16khb/nestjs-async-utils

npm version License: MIT TypeScript NestJS

English | 한국어

NestJS async operation control library. Apply retry, timeout, and concurrency limiting to methods declaratively using @Retryable, @Timeout, and @ConcurrencyLimit decorators.

Features

  • @Retryable - Automatic retry on method failure (with exponential backoff)
  • @Timeout - Limit method execution time
  • @ConcurrencyLimit - Limit concurrent executions of the same method
  • Decorator Composition - Combine multiple decorators
  • Global Configuration - Set defaults via forRoot()/forRootAsync()
  • Custom Logger - Support for custom LoggerService or simple function logger
  • Core Utilities - Framework-agnostic retry, pTimeout, pLimit functions
  • Zero Dependencies - Only NestJS and rxjs as peerDependencies

Installation

npm install @m16khb/nestjs-async-utils

# pnpm
pnpm add @m16khb/nestjs-async-utils

Requirements

  • Node.js >= 20.0.0
  • NestJS >= 10.0.0
  • TypeScript >= 5.7

Quick Start

1. Register Module

// app.module.ts
import { Module } from '@nestjs/common';
import { AsyncUtilsModule } from '@m16khb/nestjs-async-utils/nestjs';

@Module({
  imports: [
    AsyncUtilsModule.forRoot({
      defaultRetries: 3,
      defaultTimeout: 30000,
      defaultConcurrency: 10,
      enableLogging: process.env.NODE_ENV === 'development',
    }),
  ],
})
export class AppModule {}

2. Use Decorators

// payment.service.ts
import { Injectable } from '@nestjs/common';
import { Retryable, Timeout, ConcurrencyLimit } from '@m16khb/nestjs-async-utils/nestjs';

@Injectable()
export class PaymentService {
  @ConcurrencyLimit(5)       // Max 5 concurrent executions
  @Retryable({ retries: 3 }) // Retry up to 3 times on failure
  @Timeout(5000)             // 5 second timeout per attempt
  async processPayment(orderId: string): Promise<PaymentResult> {
    return this.paymentGateway.charge(orderId);
  }
}

NestJS Decorators

@Retryable

Automatically retries on method failure.

import { Retryable } from '@m16khb/nestjs-async-utils/nestjs';
import { exponentialBackoff } from '@m16khb/nestjs-async-utils/core';

@Injectable()
export class EmailService {
  // Default options (3 retries)
  @Retryable()
  async sendEmail(to: string, subject: string): Promise<void> {
    return this.mailProvider.send({ to, subject });
  }

  // Custom options
  @Retryable({
    retries: 5,
    strategy: exponentialBackoff(100, 5000),
    retryWhen: (error) => error.name !== 'ValidationError',
    enableLogging: true,
    onRetry: (attempt, error, delay) => {
      console.log(`Retry ${attempt}: ${error.message}`);
    },
  })
  async sendBulkEmail(emails: string[]): Promise<void> {
    // ...
  }
}

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | retries | number | 3 | Maximum retry attempts | | strategy | RetryStrategy | exponentialBackoff() | Backoff strategy function | | retryWhen | (error: Error) => boolean | - | Retry condition filter | | retryOn | ErrorClass[] | - | Retry only on specific error classes | | enableLogging | boolean | false | Enable logging | | onRetry | (attempt, error, delay) => void | - | Retry callback |

@Timeout

Limits method execution time.

import { Timeout } from '@m16khb/nestjs-async-utils/nestjs';

@Injectable()
export class ReportService {
  // Simple usage
  @Timeout(10000)
  async generateReport(): Promise<Report> {
    return this.reportEngine.generate();
  }

  // Detailed options
  @Timeout({
    milliseconds: 30000,
    message: 'Report generation timed out',
    enableLogging: true,
    onTimeout: (methodName, timeout) => {
      console.log(`${methodName} timed out after ${timeout}ms`);
    },
  })
  async generateLargeReport(): Promise<Report> {
    // ...
  }
}

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | milliseconds | number | 30000 | Timeout duration (ms) | | message | string | - | Timeout error message | | enableLogging | boolean | false | Enable logging | | onTimeout | (methodName, timeout) => void | - | Timeout callback |

@ConcurrencyLimit

Limits concurrent executions of the same method.

import { ConcurrencyLimit } from '@m16khb/nestjs-async-utils/nestjs';

@Injectable()
export class ExternalApiService {
  // Simple usage - max 3 concurrent executions
  @ConcurrencyLimit(3)
  async fetchData(id: string): Promise<Data> {
    return this.httpService.get(`/api/data/${id}`);
  }

  // With queue timeout
  @ConcurrencyLimit({
    limit: 5,
    queueTimeout: 10000, // Error if slot not acquired within 10s
    enableLogging: true,
  })
  async processRequest(req: Request): Promise<Response> {
    // ...
  }
}

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | limit | number | 10 | Maximum concurrent executions | | queueTimeout | number | - | Queue timeout (ms) | | enableLogging | boolean | false | Enable logging |

Decorator Composition

Note the execution order when combining decorators:

// Execution order: ConcurrencyLimit -> Retryable -> Timeout (bottom to top)
@ConcurrencyLimit(5)       // 3. Acquire concurrency slot
@Retryable({ retries: 3 }) // 2. Retry logic
@Timeout(5000)             // 1. Apply timeout to each attempt
async myMethod() {}

Execution flow:

  1. Wait for concurrency slot
  2. Start execution after acquiring slot
  3. Retry on failure (up to 3 times)
  4. Apply 5 second timeout to each attempt

Module Configuration

forRoot (Synchronous)

AsyncUtilsModule.forRoot({
  defaultRetries: 5,
  defaultTimeout: 10000,
  defaultConcurrency: 20,
  enableLogging: true,
})

forRootAsync (Asynchronous)

AsyncUtilsModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => ({
    defaultRetries: config.get('ASYNC_RETRIES', 3),
    defaultTimeout: config.get('ASYNC_TIMEOUT', 30000),
    enableLogging: config.get('ASYNC_LOGGING', false),
  }),
  inject: [ConfigService],
})

Custom Logger

You can use a custom logger that implements the NestJS LoggerService interface.

import { ConsoleLogger } from '@nestjs/common';

// Custom logger implementation
class CustomLogger extends ConsoleLogger {
  log(message: string, context?: string) {
    super.log(`[AsyncUtils] ${message}`, context);
  }
}

// Module configuration
AsyncUtilsModule.forRoot({
  logger: new CustomLogger(),
  enableLogging: true,
})

You can also use a simple function logger:

AsyncUtilsModule.forRoot({
  loggerFn: (message, context) => {
    console.log(`[${context}] ${message}`);
  },
  enableLogging: true,
})

| Option | Type | Default | Description | |--------|------|---------|-------------| | logger | LoggerService | NestJS Logger | Custom logger (LoggerService implementation) | | loggerFn | (message, context) => void | - | Simple function logger (takes precedence over logger) |

Global Module Configuration

By default, AsyncUtilsModule is registered as a global module. Set isGlobal: false to use it only in specific modules.

// Default: Global module (available throughout the app)
AsyncUtilsModule.forRoot()  // isGlobal: true (default)

// Use only in specific module
@Module({
  imports: [
    AsyncUtilsModule.forRoot({
      isGlobal: false, // Only available in this module
    }),
  ],
})
export class FeatureModule {}

Works the same with forRootAsync:

AsyncUtilsModule.forRootAsync({
  isGlobal: false,
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => ({
    defaultTimeout: config.get('ASYNC_TIMEOUT'),
  }),
  inject: [ConfigService],
})

Error Handling

import {
  RetryError,
  TimeoutError,
  QueueTimeoutError,
} from '@m16khb/nestjs-async-utils/nestjs';

try {
  await this.paymentService.processPayment(orderId);
} catch (error) {
  if (TimeoutError.isTimeoutError(error)) {
    console.log(`Timeout: ${error.milliseconds}ms`);
  } else if (RetryError.isRetryError(error)) {
    console.log(`Failed after ${error.attempts} attempts`);
  } else if (QueueTimeoutError.isQueueTimeoutError(error)) {
    console.log(`Queue timeout: ${error.queueTimeout}ms`);
  }
}

Core Utilities

Framework-agnostic utilities can be used directly:

import { retry, pLimit, pTimeout, wait } from '@m16khb/nestjs-async-utils/core';

// Retry
const data = await retry(
  () => fetch('https://api.example.com/data').then(r => r.json()),
  { attempts: 3 }
);

// Concurrency limit
const limit = pLimit(5);
const results = await Promise.all(
  urls.map(url => limit(() => fetch(url)))
);

// Timeout
const result = await pTimeout(longRunningOperation(), { milliseconds: 5000 });

// Delay
await wait(1000);

Backoff Strategies

import {
  exponentialBackoff,
  exponentialBackoffWithJitter,
  linearBackoff,
  fixedDelay,
} from '@m16khb/nestjs-async-utils/core';

// Exponential: 100ms -> 200ms -> 400ms (max 10s)
exponentialBackoff(100, 10000, 2);

// With jitter (prevents thundering herd)
exponentialBackoffWithJitter(100, 10000, 2, 0.1);

// Fixed delay
fixedDelay(1000);

Modular Imports

Import only what you need for tree-shaking optimization:

// NestJS decorators
import {
  AsyncUtilsModule,
  Retryable,
  Timeout,
  ConcurrencyLimit
} from '@m16khb/nestjs-async-utils/nestjs';

// Core utilities
import { retry, pLimit, pTimeout } from '@m16khb/nestjs-async-utils/core';

// Specific modules
import { retry, exponentialBackoff } from '@m16khb/nestjs-async-utils/retry';
import { pLimit } from '@m16khb/nestjs-async-utils/concurrency';
import { pTimeout } from '@m16khb/nestjs-async-utils/timeout';

TypeScript

import type {
  // NestJS types
  AsyncUtilsModuleOptions,
  AsyncUtilsModuleAsyncOptions,
  RetryableOptions,
  TimeoutOptions,
  ConcurrencyLimitOptions,
  MethodConcurrencyState,
  // Core types
  RetryOptions,
  RetryStrategy,
  LimitFunction,
} from '@m16khb/nestjs-async-utils/nestjs';

// Service imports
import {
  AsyncUtilsLoggerService,
  ConcurrencyManagerService,
} from '@m16khb/nestjs-async-utils/nestjs';

Performance

  • Decorator overhead: < 1ms per call
  • Concurrency: Accurate limiting with 1,000+ concurrent requests
  • Bundle size: < 1KB (gzipped, nestjs module)

License

MIT