Skip to content

Asena Redis

Redis integration for AsenaJS — service client with built-in multi-pod WebSocket transport. Your @Redis decorated service gives you full Redis operations with automatic IoC registration. For multi-pod deployments, RedisTransport synchronizes WebSocket messages across instances via Redis pub/sub.

Features

  • Decorator-Based Setup - @Redis decorator handles IoC registration and connection lifecycle
  • Dual Adapter Support - Bun native RedisClient (default) and redis (node-redis) package
  • Multi-Pod WebSocket Transport - Synchronize WebSocket messages across pods via Redis pub/sub
  • Full Redis Operations - String, Hash, Set, Key, and raw command support
  • Binary Data Support - ArrayBuffer and Uint8Array transport with Base64 encoding
  • Zero Runtime Dependencies - Only peer deps (asena, reflect-metadata)

Installation

bash
bun add @asenajs/asena-redis

For node-redis adapter (optional):

bash
bun add @asenajs/asena-redis redis

Requirements:

Quick Start

1. Create Redis Service

typescript
import { Redis, AsenaRedisService } from '@asenajs/asena-redis';

@Redis({
  config: { url: 'redis://localhost:6379' },
  name: 'AppRedis',
})
export class AppRedis extends AsenaRedisService {

  async getOrSet(key: string, factory: () => Promise<string>, ttl?: number): Promise<string> {
    const cached = await this.get(key);

    if (cached) return cached;

    const value = await factory();
    await this.set(key, value, ttl);

    return value;
  }

}

Asena automatically discovers it — that's it.

2. Inject and Use

typescript
import { Service } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';

@Service('CacheService')
export class CacheService {

  @Inject('AppRedis')
  private redis: AppRedis;

  async getUserName(id: string): Promise<string> {
    return this.redis.getOrSet(`user:${id}`, async () => {
      // fetch from database...
      return 'John';
    }, 60);
  }

}

TIP

The @Redis decorator automatically handles connection during IoC initialization and disconnection on server shutdown. You don't need to manage the lifecycle manually.

Adapter Selection

By default, @asenajs/asena-redis uses Bun's native RedisClient. For environments requiring the redis (node-redis) package:

typescript
@Redis({
  config: { url: 'redis://localhost:6379' },
  adapter: 'node-redis',
})
export class AppRedis extends AsenaRedisService {}
AdapterPackageBest For
'bun' (default)None (Bun built-in)Bun runtime, maximum performance
'node-redis'redis ^5.11.0Node.js compatibility, Redis modules

API Reference

String Operations

MethodParametersReturnsDescription
get(key)key: stringstring | nullGet string value
set(key, value, ttl?)key: string, value: string, ttl?: numbervoidSet value with optional TTL (seconds)
del(...keys)...keys: string[]numberDelete keys, returns count
exists(key)key: stringbooleanCheck if key exists
incr(key)key: stringnumberIncrement counter
decr(key)key: stringnumberDecrement counter
expire(key, seconds)key: string, seconds: numbervoidSet expiration
ttl(key)key: stringnumberGet remaining TTL
keys(pattern)pattern: stringstring[]Find keys by pattern

Hash Operations

MethodParametersReturnsDescription
hget(key, field)key: string, field: stringstring | nullGet hash field
hmset(key, fields)key: string, fields: string[]voidSet multiple fields (['f1', 'v1', 'f2', 'v2'])
hmget(key, fields)key: string, fields: string[](string | null)[]Get multiple fields

Set Operations

MethodParametersReturnsDescription
sadd(key, member)key: string, member: stringnumberAdd member to set
srem(key, member)key: string, member: stringnumberRemove member from set
smembers(key)key: stringstring[]Get all members
sismember(key, member)key: string, member: stringbooleanCheck membership

Raw & Lifecycle

MethodParametersReturnsDescription
send(command, args)command: string, args: string[]anyExecute raw Redis command
clientRedisClientAdapterAccess underlying client
createSubscriber()RedisClientAdapterCreate duplicate connection for pub/sub
testConnection()booleanReturns true if connected
disconnect()voidClose connection

Configuration

RedisConfig

typescript
interface RedisConfig {
  // Connection
  url?: string;                // redis[s]://[[username][:password]@][host][:port][/db]
  host?: string;               // default: 'localhost'
  port?: number;               // default: 6379
  username?: string;
  password?: string;
  db?: number;

  // Timeouts & Reconnection
  connectionTimeout?: number;  // Connection timeout in ms (default: 10000)
  idleTimeout?: number;        // Idle timeout in ms (Bun only, default: 0)
  autoReconnect?: boolean;     // Auto-reconnect on disconnect (default: true)
  maxRetries?: number;         // Max reconnection attempts (default: 10)

  // Behavior
  enableOfflineQueue?: boolean;     // Queue commands when disconnected (default: true)
  enableAutoPipelining?: boolean;   // Automatic command pipelining (Bun only, default: true)

  // TLS
  tls?: boolean | TLSOptions;

  // Identification
  name?: string;               // Service name for logging
}

Bun-Only Features

idleTimeout and enableAutoPipelining are Bun-only features and are silently ignored when using the node-redis adapter.

@Redis Decorator Options

typescript
@Redis({
  config: RedisConfig,         // Redis connection configuration
  adapter?: 'bun' | 'node-redis', // Client adapter (default: 'bun')
  name?: string,               // Service name for IoC registration
})

Multi-Pod WebSocket Transport

RedisTransport synchronizes WebSocket messages across multiple server instances using Redis pub/sub. This enables room-based messaging, broadcasting, and direct socket messaging to work seamlessly across pods.

Setup

Configure the transport in your @Config class's transport() method:

typescript
import { Config } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
import { ConfigService } from '@asenajs/hono-adapter'; // or '@asenajs/ergenecore'
import { RedisTransport } from '@asenajs/asena-redis';

@Config()
export class AppConfig extends ConfigService {

  @Inject('AppRedis')
  private redis: AppRedis;

  public transport() {
    return new RedisTransport(this.redis);
  }

}

Or without an existing Redis service:

typescript
public transport() {
  return new RedisTransport({ url: 'redis://localhost:6379' });
}

How It Works

Each server instance gets a unique pod ID. When a WebSocket message is published:

  1. The message is delivered locally via server.publish()
  2. The message is sent to Redis pub/sub with the originating pod ID
  3. Other pods receive the message and deliver it to their local sockets
  4. Messages from the same pod are deduplicated automatically

Options

typescript
new RedisTransport(source, {
  channel: 'asena:ws:transport', // Redis pub/sub channel (default)
});

No Code Changes Needed

Your WebSocket services, Ulak messaging, and room management work exactly the same with RedisTransport. The transport layer is transparent — just configure it in @Config and multi-pod support is enabled automatically.

Best Practices

1. Name Your Redis Services

typescript
// ✅ Good: Named service for clear IoC registration
@Redis({
  config: { url: 'redis://localhost:6379' },
  name: 'AppRedis',
})
export class AppRedis extends AsenaRedisService {}

// ❌ Bad: Unnamed service (defaults to class name, but less explicit)
@Redis({ config: { url: 'redis://localhost:6379' } })
export class AppRedis extends AsenaRedisService {}

2. Use Cache Patterns

typescript
// ✅ Good: getOrSet pattern for caching
async getOrSet(key: string, factory: () => Promise<string>, ttl?: number) {
  const cached = await this.get(key);
  if (cached) return cached;

  const value = await factory();
  await this.set(key, value, ttl);
  return value;
}

3. Health Checks

typescript
// ✅ Good: Use testConnection() in health endpoints
@Get('/health')
async health(context: Context) {
  const redisOk = await this.redis.testConnection();
  return context.send({ redis: redisOk ? 'up' : 'down' });
}

Next Steps:

Released under the MIT License.