@fluojs/redis
v2.0.2
Published
App-scoped Redis lifecycle management, raw client injection, and RedisService facade for Fluo.
Maintainers
Readme
@fluojs/redis
Node.js support is >=24.0.0 <27. See Node.js support and migration before upgrading.
Shared Redis connection layer for fluo. It provides a default app-scoped ioredis client plus optional named clients, all managed by the application lifecycle.
Table of Contents
Installation
npm install @fluojs/redis ioredisWhen to Use
- When you need a shared Redis connection across multiple modules (caching, queues, throttlers).
- When you want automatic connection management (connect on bootstrap, quit on shutdown).
- When you need a JSON-aware facade for common key-value operations.
Quick Start
Register the Module
Use RedisModule.forRoot(options) to register the default Redis client and RedisService facade.
RedisModule.forRoot(...) is intentionally synchronous and always creates a new ioredis client from Redis constructor options; it does not adopt an externally constructed client. When migrating from NestJS async dynamic modules such as forRootAsync(...), resolve secrets, environment-specific hosts, and TLS options at the application boundary first, then pass the final Redis options into forRoot(...). Keep externally constructed clients outside this module and close them from the application lifecycle. fluo does not defer Redis module wiring to an async factory hidden inside the module graph.
import { Module } from '@fluojs/core';
import { RedisModule } from '@fluojs/redis';
@Module({
imports: [
RedisModule.forRoot({
host: 'localhost',
port: 6379,
}),
],
})
export class AppModule {}Use the Redis Service
Inject RedisService for high-level operations or REDIS_CLIENT for the raw ioredis instance.
RedisService.get() attempts JSON parsing and falls back to the raw string; missing keys return null. RedisService.set() serializes values with JSON.stringify(): finite positive integer TTLs use Redis EX, while finite positive fractional TTLs use PX with milliseconds rounded up. Omit the TTL or pass a non-positive or non-finite value for a persistent key.
import { Inject } from '@fluojs/core';
import { RedisService } from '@fluojs/redis';
@Inject(RedisService)
export class CacheRepository {
constructor(private readonly redis: RedisService) {}
async saveUser(id: string, user: object) {
await this.redis.set(`user:${id}`, user, 3600);
}
async getUser(id: string) {
return await this.redis.get(`user:${id}`);
}
}Common Patterns
Lifecycle Ownership
Every RedisModule.forRoot(...) registration creates a new client that @fluojs/redis owns, including named clients registered through RedisModule.forRoot({ name, ... }). The module never adopts an existing client instance. Registration identity is application-wide: only one unnamed default registration and one registration for each trimmed name are allowed. Compatible duplicate package copies share the exported default/named token identities and the registration marker, so mixed-copy duplicates are rejected during bootstrap before creating a Redis client. Ownership remains isolated per application; private lifecycle/guard tokens and the default RedisService constructor token keep their existing policy.
- Fluo always forces
lazyConnect: true, even if callers cast options manually, so sockets open during application bootstrap instead of import time. - During bootstrap, the lifecycle service only calls
connect()while the client is still in iorediswaitstate. - Lifecycle-owned
connect()andquit()calls are bounded by package timeouts (10_000ms by default) so bootstrap and shutdown do not wait forever on a stalled Redis command. Override them withlifecycle.connectTimeoutMsandlifecycle.quitTimeoutMs; pass0only when the host process intentionally owns an unbounded wait. - During shutdown, ready/connecting clients attempt
quit()first for graceful teardown, while monitoring, wait, and closed-transition states usedisconnect()directly. - If
quit()fails, Fluo falls back todisconnect()and only rethrows when the client still remains open afterward.
Option Normalization
RedisModuleOptions is the caller-facing input to RedisModule.forRoot(...), not the final ioredis constructor shape. It accepts ordinary ioredis options except lazyConnect and name, then adds four Fluo-only fields:
nameidentifies the Fluo registration and its DI tokens. It never becomes theioredisconstructorname. Each trimmed name is an application-wide ownership identity and can be registered only once.globalcontrols module visibility. The default registration is global unless set tofalse; named registrations are always scoped and rejectglobal: true.lifecycleconfigures Fluo-ownedconnect()andquit()timeout guardrails.sentinelNamesupplies the ioredis Sentinel master name because the inputnamefield is reserved for Fluo registration identity.
Normalization happens before client construction in this order:
- Fluo removes
name,global,lifecycle, andsentinelNamefrom the ordinary ioredis options. - It trims the registration
name, then normalizes and validates the lifecycle timeouts. - It rejects a blank registration name and rejects
global: trueon a named registration. - It maps
sentinelName, when present, to the ioredis constructornameand producesRedisClientOptions. - The provider creates the client with
{ ...clientOptions, lazyConnect: true }. This final assignment always wins, including when a caller bypasses the type restriction and castslazyConnect: falseinto the input.
RedisClientOptions therefore describes the normalized constructor-facing options consumed by the provider factory: ordinary ioredis options plus an optional Sentinel name, but no Fluo-only fields and no caller-controlled lazyConnect. It is not a synonym for the RedisModuleOptions accepted by forRoot(...).
Named Clients
Use RedisModule.forRoot({ name, ...options }) when one application needs more than one Redis connection. RedisModule.forRoot(options) without name provides the default REDIS_CLIENT and RedisService aliases, and named registrations are resolved with getRedisClientToken(name) and getRedisServiceToken(name).
- Omit
namewhen you want the default aliases:REDIS_CLIENT/RedisService. - Pass
namewhen you want the named helpers:getRedisClientToken(name)/getRedisServiceToken(name). nameidentifies the Fluo registration only. For an ioredis Sentinel master name, passsentinelName; Fluo forwards it to ioredis as its constructornameoption without changing the registration tokens.- Named clients follow the same bootstrap/shutdown contract as the default client; only the default registration exports the
REDIS_CLIENT/RedisServicealiases. - Names are trimmed, and blank or whitespace-only names are rejected by token/component helpers.
import { Module, Inject } from '@fluojs/core';
import type Redis from 'ioredis';
import {
getRedisClientToken,
getRedisServiceToken,
RedisModule,
RedisService,
} from '@fluojs/redis';
const ANALYTICS_REDIS = getRedisServiceToken('analytics');
const ANALYTICS_REDIS_CLIENT = getRedisClientToken('analytics');
@Module({
imports: [
RedisModule.forRoot({ host: 'localhost', port: 6379 }),
RedisModule.forRoot({ name: 'analytics', host: 'localhost', port: 6380 }),
RedisModule.forRoot({ name: 'sentinel-cache', sentinelName: 'mymaster', sentinels: [{ host: 'localhost', port: 26379 }] }),
],
})
export class AppModule {}
@Inject(RedisService, ANALYTICS_REDIS, ANALYTICS_REDIS_CLIENT)
export class AnalyticsStore {
constructor(
private readonly defaultRedis: RedisService,
private readonly analyticsRedis: RedisService,
private readonly analyticsClient: Redis,
) {}
}Raw Client Access
If you need advanced Redis commands (pipelines or Lua scripts), inject the raw client directly.
If you already injected RedisService, call redis.getRawClient() to access the same underlying ioredis instance.
The raw client retains the full ioredis API, including duplicate, watch, and multi. @fluojs/cache-manager atomic updates consume that existing seam; RedisService does not gain an update method and Redis module options do not gain atomicUpdates. Opt in on the cache registration with redis: { atomicUpdates: true }. Cache-manager owns the exported optional RedisAtomicClient / RedisAtomicTransaction structural types and its isolated duplicate(...) connections, disconnecting each in finally. It never closes the shared injected raw client. This is separate from an application-created duplicate, whose lifecycle remains application-owned. Redis topology, namespace metadata, TTL, retry, and cancellation requirements belong to the linked cache API owner. Operation-owned connections disable reconnection: connection loss propagates the client error instead of committing on a replacement connection without WATCH. The shared client retains its reconnection policy.
Redis Pub/Sub is the exception to ordinary shared-client reuse: Redis switches subscribed connections into subscribe mode, so do not use the lifecycle-managed REDIS_CLIENT or a RedisService.getRawClient() result as both publisher and subscriber. Create a dedicated subscriber connection with client.duplicate() (or an explicitly named RedisModule.forRoot({ name: 'subscriber', ... }) registration) and let that connection have its own lifecycle owner.
If you use client.duplicate(), the duplicate is application-owned: connect it, subscribe with it, and close it from your own shutdown path. If you want fluo to own subscriber startup/shutdown timeouts, prefer a named registration and inject it with getRedisClientToken(name).
import { Inject } from '@fluojs/core';
import { REDIS_CLIENT } from '@fluojs/redis';
import type Redis from 'ioredis';
@Inject(REDIS_CLIENT)
export class AdvancedService {
constructor(private readonly client: Redis) {}
async executeComplex() {
return await this.client.pipeline().set('foo', 'bar').get('foo').exec();
}
}import { Inject, Module } from '@fluojs/core';
import { getRedisClientToken, RedisModule } from '@fluojs/redis';
import { RedisPubSubMicroserviceTransport } from '@fluojs/microservices';
import type Redis from 'ioredis';
const COMMAND_REDIS = getRedisClientToken();
const SUBSCRIBER_REDIS = getRedisClientToken('subscriber');
@Inject(COMMAND_REDIS, SUBSCRIBER_REDIS)
export class PubSubTransportFactory {
constructor(
private readonly commandClient: Redis,
private readonly subscriberClient: Redis,
) {}
createTransport() {
return new RedisPubSubMicroserviceTransport({
publishClient: this.commandClient,
subscribeClient: this.subscriberClient,
});
}
}
@Module({
imports: [
RedisModule.forRoot({ host: 'localhost', port: 6379 }),
RedisModule.forRoot({ name: 'subscriber', host: 'localhost', port: 6379 }),
],
providers: [PubSubTransportFactory],
})
export class RedisConnectionsModule {}@Inject(...) only declares the constructor tokens of a class; it does not register that class. fluo resolves providers from explicit module registration, so PubSubTransportFactory must be listed in the providers array of the module that imports the Redis registrations it depends on. A decorated class that no module lists is not resolvable.
Public API Overview
Core
RedisModule: Registers Redis clients and lifecycle hooks.RedisModule.forRoot(options): Registers the default Redis client plusRedisServicefacade whennameis omitted, or an additional named Redis client whennameis provided. The default registration is global by default; named registrations are scoped. Uselifecycle.connectTimeoutMsandlifecycle.quitTimeoutMsto bound Fluo-owned connection startup and shutdown.- Lifecycle hooks are configured only through
RedisModule.forRoot(...).lifecycle; the internal lifecycle service is intentionally not part of the public API. RedisService: Facade with JSON codec support andget/set/delmethods.REDIS_CLIENT: DI token for the underlyingioredisinstance.DEFAULT_REDIS_CLIENT_NAME: Stable default Redis client name.getRedisClientToken(name): DI token helper for a named raw client. Omittingnamereturns the defaultREDIS_CLIENTtoken.getRedisServiceToken(name): DI token helper for a namedRedisServicefacade. Omittingnamereturns the defaultRedisServicetoken.getRedisComponentId(name): Status/dependency id helper used by Redis-consuming packages (redis.default,redis.cache, etc.).createRedisPlatformStatusSnapshot(input): Adapts Redis connection state into Fluo's platform health/readiness snapshot contract.
Types
DefaultRedisModuleOptions: Options accepted by the unnamed default Redis registration, including optional global alias visibility and lifecycle timeout controls.NamedRedisModuleOptions: Options accepted by additional named Redis registrations, including requirednameand scoped lifecycle timeout controls.RedisModuleOptions: Caller-facing union accepted byRedisModule.forRoot(...). It combines ordinary ioredis options exceptlazyConnectandnamewith the Fluo-onlyname,global,lifecycle, andsentinelNamefields.RedisClientOptions: Normalized constructor-facing options after Fluo removes its module-only fields and mapssentinelNameto the ioredis Sentinelname. The provider addslazyConnect: trueafterward as the final override.RedisLifecycleOptions: Optional timeout controls for Fluo-ownedconnect()andquit()lifecycle commands.PersistencePlatformStatusSnapshot,RedisStatusAdapterInput: Status snapshot input/output types.
Related Packages
@fluojs/cache-manager: Uses this package for Redis-backed caching.@fluojs/queue: Uses this package for distributed job processing.@fluojs/throttler: Uses this package for distributed rate limiting.
Example Sources
packages/redis/src/module.test.ts: Module lifecycle and DI wiring.packages/redis/src/public-api.test.ts: Root-barrel export guard for the documented Redis surface.packages/redis/src/redis-service.ts: Facade implementation and codec logic.
Compatible service copies
Default public Redis service classes support compatible-copy class injection. Named Redis clients and services remain name-isolated.
