Skip to content

Configuration ​

Plugin Options ​

OptionTypeDefaultDescription
clientstring'default'Named Redis client to use
keyPrefixstring'_sem:'Prefix for semaphore keys (must be non-empty)
defaultTtlnumber30000Permit lease in ms - a crashed holder frees its seat after this
maxTtlnumber300000Hard cap for defaultTtl and per-call ttl
retry.maxRetriesnumber3Retries after the initial acquire attempt
retry.initialDelaynumber100First backoff delay in ms
retry.maxDelaynumber3000Backoff ceiling in ms
retry.multipliernumber2Exponential backoff multiplier (≥ 1)
autoRenew.enabledbooleantrueRenew held permits automatically
autoRenew.intervalFractionnumber0.5Renewal interval as a fraction of the TTL (strictly between 0 and 1)

All options are validated fail-fast at registration (and again in the service constructor for hand-wired setups): non-positive timings, defaultTtl > maxTtl, an interval fraction outside (0, 1), or an empty keyPrefix throw InvalidSemaphoreConfigError before the application starts.

Synchronous Setup ​

typescript
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-redisx/core';
import { SemaphorePlugin } from '@nestjs-redisx/semaphore';

@Module({
  imports: [
    RedisModule.forRoot({
      clients: {
        host: 'localhost',
        port: 6379,
      },
      plugins: [
        new SemaphorePlugin({
          defaultTtl: 30_000, // permit lease; a crashed holder frees its seat after this
          maxTtl: 300_000, // hard cap for per-call ttl values
          retry: { maxRetries: 3, initialDelay: 100, maxDelay: 3_000, multiplier: 2 },
          autoRenew: { enabled: true, intervalFraction: 0.5 },
        }),
      ],
    }),
  ],
})
export class AppModule {}

Async Registration ​

Load configuration through NestJS DI (ConfigService, Vault, …):

typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisModule } from '@nestjs-redisx/core';
import { SemaphorePlugin } from '@nestjs-redisx/semaphore';

@Module({
  imports: [
    RedisModule.forRoot({
      clients: { host: 'localhost', port: 6379 },
      plugins: [
        SemaphorePlugin.registerAsync({
          imports: [ConfigModule],
          inject: [ConfigService],
          useFactory: (config: ConfigService) => ({
            defaultTtl: config.get<number>('SEMAPHORE_TTL_MS') ?? 30_000,
            keyPrefix: config.get<string>('SEMAPHORE_PREFIX') ?? '_sem:',
          }),
        }),
      ],
    }),
  ],
})
export class AppModule {}

Per-Call Options ​

Every acquisition can override the plugin defaults:

OptionDescription
ttlPermit TTL for this acquisition (capped at maxTtl)
waitTimeoutTotal wait budget in ms - stop retrying once sleeping again would exceed it
autoRenewEnable/disable renewal for this permit
retry.maxRetries, retry.initialDelayPer-call retry overrides

Choosing the TTL ​

The TTL is the crash-recovery bound: a holder that dies without releasing blocks its seat for at most the remaining TTL. With auto-renewal on (default), pick a TTL comfortably above one renewal round-trip (seconds, not milliseconds) and let renewal carry long work. With auto-renewal off, the TTL must exceed your worst-case execution time.

Capacity Discipline ​

permits is passed per call and must be the same for every caller of a key. The store enforces the capacity the acquiring call provides - two callers using different numbers for one key will observe the larger caller's view win whenever it acquires. Define the capacity once next to the key:

typescript
export const RENDER_SEMAPHORE = { key: 'reports:render', permits: 3 } as const;

Released under the MIT License.