@notross/redis-hub
v2.0.4
Published
A minimal, zero-config connection hub for Redis in Node.js
Maintainers
Readme
@notross/redis-hub
Zero-config, lazy connection hub for Redis in Node.js. Lazily creates and reuses named Redis clients (
publisher,subscriber, per-tenant, per-worker) with centralized configuration, per-client state tracking, and pluggable structured logging.
Features
- ⚡ Zero-Config Defaults: Point to
REDIS_URLorREDIS_HUB_URLand query instantly with zero boilerplate required. - 🔀 Named & Lazily Created Clients: Identical client IDs resolve to the same memoized, connected Redis instance across your entire application.
- 📦 No Export Boilerplate: Use
RedisHubstatic methods anywhere in your codebase without exporting shared instances across files. - 📊 Per-Client State Tracking: Track individual status (
ready,reconnecting,error) on a per-client basis rather than global mutable state. - 🪵 Pluggable Structured Logging: Inject your own Pino logger instance or use sensible zero-config defaults.
- 🛑 Graceful Shutdown: Drains in-flight commands using graceful
quit()disconnects with opt-inSIGTERM/SIGINTprocess hooks.
Installation
npm install @notross/redis-hub
# or
yarn add @notross/redis-hubQuick Start
Set REDIS_URL (or rely on platform environment variables on Heroku, Railway, Render, Fly, etc.) and call RedisHub.getClient(...) directly:
// publisher.ts
import { RedisHub } from '@notross/redis-hub';
const pub = await RedisHub.getClient('publisher');
await pub.publish('chat', 'hello world');// subscriber.ts — separate file, same package, zero shared variable exports
import { RedisHub } from '@notross/redis-hub';
const sub = await RedisHub.getClient('subscriber');
await sub.subscribe('chat', (message) => {
console.log('Received:', message);
});Why Named Clients? Redis Pub/Sub requires dedicated connections for publishing and subscribing—named clients manage this lifecycle automatically.
Programmatic Configuration
To set global connection options, inject a logger, or enable automatic graceful shutdown hooks, call RedisHub.config(...) once at application boot:
import { RedisHub } from '@notross/redis-hub';
import pino from 'pino';
RedisHub.config({
redis: { url: process.env.REDIS_URL },
logger: pino({ level: 'info' }),
autoShutdown: true,
});Per-Client Handles
For modules that own a specific named client, use RedisHub.handle('name') to encapsulate operations without repeating the client ID at every call site:
import { RedisHub } from '@notross/redis-hub';
const publisher = RedisHub.handle('publisher');
export async function publish(channel: string, message: string) {
const client = await publisher.client; // Memoized — connects lazily on first access
await client.publish(channel, message);
}
// Inspect status or disconnect independently
publisher.getState(); // => { status: 'ready', lastError: null, connectedAt: 1700000000000 }
await publisher.disconnect();RedisHub.handle(...) returns synchronously without triggering an immediate network call. The internal .client property is a memoized promise that connects lazily on first access.
Configuration Precedence
If RedisHub.config(...) is not called explicitly, the first getClient() access automatically resolves configuration settings in the following order (highest priority wins):
REDIS_HUB_URL— Package-specific environment variable override.REDIS_URL— Standard environment variable set by hosting platforms.- Config File — Resolved via
cosmiconfig:redis-hubkey inpackage.json.redis-hubrc(.json,.yaml,.yml,.js,.cjs,.mjs)redis-hub.config.js(.cjs,.mjs,.json)
- Local Fallback —
redis://localhost:6379(used strictly as a last resort for local development).
// redis-hub.config.json
{
"redis": {
"url": "redis://localhost:6379"
}
}Environment File Notice:
.envfiles should be loaded upstream by your application runtime (e.g., viadotenvor framework runners) before initializingRedisHub.
Logging
Inject any Pino instance, Pino child logger, or custom logger adhering to the HubLogger interface:
import { RedisHub, type HubLogger } from '@notross/redis-hub';
import pino from 'pino';
RedisHub.config({ logger: pino() }); // Standard Pino instance
RedisHub.config({ logger: myWinstonAdapter }); // Custom HubLogger adapter
RedisHub.config({ logger: false }); // Disable logging entirelytype HubLogger = {
debug: (obj: object | string, msg?: string) => void;
info: (obj: object | string, msg?: string) => void;
warn: (obj: object | string, msg?: string) => void;
error: (obj: object | string, msg?: string) => void;
};Graceful Shutdown
Enable automatic process termination handling to drain active commands before exit:
import { RedisHub } from '@notross/redis-hub';
RedisHub.config({ autoShutdown: true });This hooks SIGTERM and SIGINT signals to execute disconnectAll() gracefully before process termination.
API Reference
RedisHub.config(config: HubConfig): void
Sets global hub options.
RedisHub.config({
redis: { url: 'redis://localhost:6379' },
logger: pino(),
autoShutdown: true,
onOptionsConflict: 'warn', // 'throw' | 'warn' | 'ignore'
});RedisHub.getClient(clientId: string, options?: ClientConfig): Promise<RedisClient>
Retrieves or lazily instantiates a named Redis client instance.
RedisHub.getClientState(clientId: string): ClientState
Returns runtime status for a specific client ID ('idle' | 'connecting' | 'ready' | 'reconnecting' | 'ended' | 'error').
RedisHub.handle(clientId: string, options?: ClientConfig): ClientHandle
Returns a { client, getState(), disconnect() } handle bound to a single named client.
RedisHub.disconnect(clientId: string, options?: DisconnectOptions): Promise<void>
Gracefully disconnects a single client using quit(), falling back to destroy() after timeoutMs (default: 5000ms).
RedisHub.disconnectAll(options?: DisconnectOptions): Promise<void>
Gracefully disconnects all managed clients across the hub.
new RedisHub(config?: HubConfig)
Instantiates an isolated hub instance for testing or multi-tenant architectures requiring distinct configurations.
Migrating from v1
This release introduces breaking structural and operational updates:
- Removal of
.init(): Replaced by automatic resolution from environment variables or explicitRedisHub.config(...). - Removal of
useClient: Deprecated in favor ofRedisHub.getClient(id, options)orredisHub.getClient(...). - Nested Connection Options: Per-client Redis connection parameters must now be nested under a
rediskey.
// Legacy (v1)
import { useClient } from '@notross/redis-hub';
const publisher = await useClient('publisher', {
pingInterval: 60000,
socket: { keepAlive: true },
});
// Current (v2)
import { RedisHub } from '@notross/redis-hub';
const publisher = await RedisHub.getClient('publisher', {
redis: {
pingInterval: 60000,
socket: { keepAlive: true },
},
});Known Gotchas
Reconnect loops against Redis Enterprise / Redis Cloud in containerized environments
redis-hub passes your redis: {...} options straight through to createClient(), so this is really a node-redis behavior to be aware of rather than anything redis-hub does itself — but it's easy to lose hours to, so it's worth flagging here.
As of redis ^6.x, RESP3 is the default protocol, which in turn defaults maintNotifications to "auto". That silently opts every client into Redis Enterprise/Cloud's maintenance-notification handshake (CLIENT MAINT_NOTIFICATIONS ON), meaning the client will honor server-pushed MOVING redirects to a different address during cluster maintenance/failover. To decide what kind of address to request (internal-ip, external-fqdn, etc.), node-redis DNS-resolves your Redis host and classifies it private vs. public — a classification that can differ by network context (e.g., a Docker container's DNS/routing vs. your host machine's).
If you see clients connect fine in one environment but immediately enter a reconnect loop and crash in another — most commonly: works from a local terminal, fails inside Docker — with the same connection URL, this is the first thing to check, not a config or credentials problem.
To confirm: set REDIS_DEBUG_MAINTENANCE=1 and REDIS_EMIT_DIAGNOSTICS=1 in the failing environment. If a MOVING/MIGRATING push notification logs right before the crash loop starts, this is it.
To fix, disable it explicitly (or pin the endpoint type instead of leaving it on 'auto'):
RedisHub.config({
redis: {
url: process.env.REDIS_URL,
maintNotifications: 'disabled',
// — or, to keep the feature but stop it from guessing —
// maintEndpointType: 'external-ip',
},
});License
MIT © Ross Libby
