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 🙏

© 2026 – Pkg Stats / Ryan Hefner

redora

v0.3.1

Published

Redis SDK for NestJS with a typed command API and declarative HTTP caching (interceptor, decorator, cache keys, TTL presets), built on ioredis.

Downloads

385

Readme

Redora

A Redis SDK for NestJS — typed Redis commands, automatic serialization, eviction-aware caching, structured logging, and Redis observability. Built on ioredis.

Try it

See Redora wired end-to-end in a NestJS API — cache interceptor, remember / set / evict, TTL, and observability:

test-redora-app

The Story

Redora is inspired by Pandora, the fictional world of the Avatar movies — a place where different tribes, each with their own unique skills, live together in one connected ecosystem.

Redora is Pandora for Redis. Different Redis "tribes" live together under one roof:

  • Tribe 1 — Redis: typed command API, serialization, connection lifecycle, and a raw ioredis escape hatch.
  • Tribe 2 — Cache: HTTP interceptor caching, service-level remember / set, eviction groups (tags), TTL presets, and skip-empty-payload rules.
  • Tribe 3 — Redis Logger: pino-backed logger used by the Redis module (and injectable in your own providers).
  • Tribe 4 — Observability: health checks from INFO, diagnosis groups, and telemetry payload builders for Prometheus / OpenTelemetry configuration.

Future tribes — queues, rate limiting, distributed locking, Redis for AI — will continue to join.

Features

  • Typed Redis commands for strings, hashes, lists, sets, sorted sets, keys, PING, and INFO
  • Automatic JSON serialize / deserialize with generics (get<T>(), hGetAll<T>(), …)
  • Duration strings ('5m', '250ms') and numeric TTL with ExpiryUnits (EX vs PX)
  • Declarative HTTP caching: @Cacheable() + CacheInterceptor
  • Service cache-aside: CacheService.remember()
  • Write-through + tag index: CacheService.set() (always indexes an eviction group)
  • Group invalidation: CacheService.evict()
  • Meaningful-payload gate: empty [] / {} / null are not written
  • optional: true Redis so Nest can boot when Redis is down
  • Observability: ping, memory/client/server snapshots, stats diagnosis, telemetry metric mapping
  • Full TypeScript declarations (node ≥ 18)

Installation

npm install redora

Peer dependencies (already present in a Nest 11 app): @nestjs/common, @nestjs/core, rxjs, reflect-metadata.

npm install @nestjs/common @nestjs/core rxjs reflect-metadata

Wiring all modules

Import Redis first. Cache, logger, and observability resolve RedisService from that global module.

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import {
  CacheModule,
  DEFAULT_CACHE_TTL_SECONDS,
  ExpiryUnits,
  ObservabilityModule,
  RedisLoggerModule,
  RedisModule,
} from 'redora';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    RedisLoggerModule.forRoot({ level: 'info' }),
    RedisModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        url: config.getOrThrow<string>('REDIS_URL'),
      }),
    }),
    CacheModule.forRoot({
      prefix: 'cache',
      namespace: 'http',
      ttlSeconds: DEFAULT_CACHE_TTL_SECONDS.instant,
      expiryUnit: ExpiryUnits.SECONDS,
    }),
    ObservabilityModule.forRoot({
      healthCheck: {
        memory: { show: true },
        diagnosis: true,
      },
      telemetry: {
        prometheus: { enabled: true },
      },
    }),
  ],
})
export class AppModule {}

RedisModule, CacheModule, and ObservabilityModule are global. RedisLoggerModule.forRoot() is global unless you set isGlobal: false.

RedisModule.forRoot() already imports the logger. You can still register RedisLoggerModule.forRoot() yourself to set level.


Tribe 1 — Redis

Registering the connection

Static

RedisModule.forRoot({ host: 'localhost', port: 6379 });
RedisModule.forRoot('redis://localhost:6379');
RedisModule.forRoot(6379); // ioredis port shorthand

Async (ConfigService)

RedisModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    url: config.getOrThrow('REDIS_URL'),
    password: config.get('REDIS_PASSWORD'),
    database: Number(config.get('REDIS_DB') ?? 0),
  }),
});

Connection options

interface RedisConnectionOptions {
  host?: string;
  port?: number;
  username?: string;
  password?: string;
  database?: number; // mapped to ioredis `db`
  url?: string; // redis:// or rediss://
  keyPrefix?: string;
  maxRetriesPerRequest?: number | null;
  retryStrategy?: (times: number) => number | null;
  enableReadyCheck?: boolean;
  lazyConnect?: boolean;
  enableOfflineQueue?: boolean;
  optional?: boolean;
  tls?: { rejectUnauthorized?: boolean };
  maxRedirects?: number;
}

Soft-fail when Redis is down (optional: true)

By default ioredis reconnects and may queue commands (requests hang). optional: true sets lazy connect, fail-fast commands, limited retries:

RedisModule.forRoot({
  url: process.env.REDIS_URL,
  optional: process.env.NODE_ENV !== 'production',
});

Nest still starts. Commands throw until Redis is reachable — wrap best-effort cache calls in try/catch. CacheModule still needs RedisModule in DI; optional only softens the socket, it does not remove Redis from the graph.

RedisService vs CacheService.set

| | RedisService.set | CacheService.set | |---|---|---| | Options type | SetOptions | CacheSetOptions | | Eviction group | not used | required — indexes a tag after write | | Empty payload | written as-is | skipped (hasMeaningfulData) | | Typical use | sessions, counters, arbitrary keys | cache tribe write-through |

// Low-level: no tag
await this.redisService.set({
  key: 'session:abc',
  value: { userId: 1 },
  ttlSeconds: 3600,
});

// Cache tribe: always pass evictionGroupName
await this.cacheService.set({
  key: this.cacheService.buildKey({ namespace: 'products', key: ['item', id] }),
  value: product,
  ttlSeconds: DEFAULT_CACHE_TTL_SECONDS.short,
  evictionGroupName: `products:item:${id}`,
});

TTL on Redis writes

Numeric ttlSeconds defaults to seconds (EX). Override with ExpiryUnits.MILLISECONDS (PX).

Duration strings encode the unit. Do not pass expiryUnit together with a string (throws TimeFormatExceptions).

| Input | Redis | |---|---| | ttlSeconds omitted | no expiry | | ttlSeconds: 60 | EX 60 | | ttlSeconds: 60, expiryUnit: SECONDS | EX 60 | | ttlSeconds: 1500, expiryUnit: MILLISECONDS | PX 1500 | | ttlSeconds: '5m' | EX 300 | | ttlSeconds: '250ms' | PX 250 | | ttlSeconds: 0 or negative | throws TimeFormatExceptions |

Supported duration suffixes: ms, s, m, h, d, y (365-day years). Examples: '2s', '15m', '1h', '1d'.

Omit ttlSeconds when the key should persist until deleted.

Command API

All of the following live on RedisService.

Strings, numbers, ping

await redis.set({ key: 'greeting', value: 'hello' });
await redis.set({ key: 'session', value: { userId: 1 }, ttlSeconds: '1h' });
await redis.set({
  key: 'flash',
  value: 'gone-soon',
  ttlSeconds: 1500,
  expiryUnit: ExpiryUnits.MILLISECONDS,
});

const greeting = await redis.get('greeting');
const session = await redis.get<{ userId: number }>('session');

await redis.incr('counter');
await redis.incrBy('counter', 10);
await redis.decr('counter');
await redis.decrBy('counter', 5);

await redis.mget<User>('user:1', 'user:2');
await redis.mset({ 'config:a': { on: true }, 'config:b': 42 });
await redis.ping(); // 'PONG'

Keys

await redis.del('key1', 'key2');
await redis.unlink('big-key');
await redis.exists('key1');
await redis.expire('key1', 60);
await redis.ttl('key1'); // seconds left; -1 no expiry; -2 missing
await redis.persist('key1');
await redis.rename('old', 'new');
await redis.keys('user:*'); // avoid on large production keyspaces
const [cursor, keys] = await redis.scan(0, { match: 'user:*', count: 100 });

Hashes

await redis.hSet('profile:1', 'name', 'Neba');
await redis.hSet('profile:1', { age: 30, city: 'Addis Ababa' });
const name = await redis.hGet<string>('profile:1', 'name');
const profile = await redis.hGetAll<Profile>('profile:1');
await redis.hDel('profile:1', 'age');
await redis.hExists('profile:1', 'name');
await redis.hKeys('profile:1');
await redis.hVals<string>('profile:1');
await redis.hLen('profile:1');

Lists

await redis.lPush('queue', job1, job2);
await redis.rPush('log', entry);
const job = await redis.lPop<Job>('queue');
const last = await redis.rPop<Entry>('log');
const page = await redis.lRange<Job>('queue', 0, 9);
const size = await redis.lLen('queue');

Sets

await redis.sAdd('tags', 'redis', 'nestjs');
await redis.sRem('tags', 'redis');
const tags = await redis.sMembers<string>('tags');
const isMember = await redis.sIsMember('tags', 'nestjs');
const count = await redis.sCard('tags');

Sorted sets (ZMember is { score, member })

await redis.zAdd(
  'leaderboard',
  { score: 100, member: 'alice' },
  { score: 90, member: 'bob' },
);
await redis.zRem('leaderboard', 'bob');
const top = await redis.zRange('leaderboard', 0, 9);
const topWithScores = await redis.zRange('leaderboard', 0, 9, true);
const reversed = await redis.zRevRange('leaderboard', 0, 9);
const score = await redis.zScore('leaderboard', 'alice');

INFO

const raw = await redis.info(); // entire INFO dump
const memory = await redis.info('memory');

Prefer ObservabilityService for parsed health/diagnosis rather than scraping INFO yourself.

Raw ioredis client

import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { REDIS_CLIENT } from 'redora';

@Injectable()
export class PubSubService {
  constructor(@Inject(REDIS_CLIENT) private readonly client: Redis) {}

  publish(channel: string, message: string) {
    return this.client.publish(channel, message);
  }
}

Tribe 3 — Redis Logger

RedisLoggerService is a small pino wrapper. Redis connection events (connect, error, reconnecting, end) go through it.

RedisLoggerModule.forRoot({
  isGlobal: true, // default true
  level: 'debug', // debug | info | warn | error
});

Inject it anywhere:

constructor(private readonly redisLogger: RedisLoggerService) {}

this.redisLogger.log('cache warmed');
this.redisLogger.warn('fallback to database');
this.redisLogger.error(err.message);
this.redisLogger.debug('ttl sync');

Token REDIS_LOGGER_OPTIONS is exported if you need the raw options object.


Tribe 2 — Cache

Recommended consumer pattern

| Endpoint | Mechanism | Eviction group | |---|---|---| | GET collection (findAll) | @Cacheable + CacheInterceptor | one list group for every list variant (query, page, sort) | | GET one (findOne) | CacheService.remember in the service | per-id item group | | POST | evict(list group) only | — | | PATCH | evict(list group) then set item + item group | item group shared with remember | | DELETE | evict([list group, item group]) | — |

Do not put @Cacheable on GET by id. That would store cache:…:GET:/products/3 while remember/set use cache:products:item:3. PATCH would update one key and leave the HTTP key stale.

Keep group names in constants so the decorator and evict cannot drift:

export const PRODUCTS_LIST_EVICTION_GROUP = 'products:list';

export function productsItemEvictionGroup(id: number): string {
  return `products:item:${id}`;
}

What gets written (meaningful data)

Interceptor, remember, and CacheService.set all refuse payloads that are not meaningful:

Not cached (response still returned): null, undefined, '', NaN, [], {}, { items: [] }, [{}], { user: null }, empty Buffer, invalid Date.

Cached: 'hello', 0, false, [1, 2], { id: 1 }, { success: false, count: 0 }, { empty: [], full: [1] }.

remember still returns the loader result when it skips the write (for example []). A thrown NotFoundException is never stored.

CacheModule

forRoot registers defaults. The interceptor, CacheService.set, and CacheService.remember all shallow-merge { ...moduleDefaults, ...callOrDecorator }. Call / decorator fields win. undefined on the call does not wipe a module default (set / remember skip those keys).

There is no module-level evictionGroupName. A shared default tag would mix unrelated keys. The group must be set on @Cacheable, set, and remember. Missing or empty group throws CacheEvictionException.

CacheModule.forRoot({
  prefix: 'cache',
  namespace: 'http',
  ttlSeconds: DEFAULT_CACHE_TTL_SECONDS.short,
  expiryUnit: ExpiryUnits.SECONDS,
});

| Option | Role | |---|---| | ttlSeconds / expiryUnit | Default TTL when @Cacheable(), set, or remember omit them | | prefix / namespace / ignoreQuery / vary* / hash / maxLength | Default HTTP key pieces for the interceptor |

@Cacheable({
  namespace: 'products',
  evictionGroupName: PRODUCTS_LIST_EVICTION_GROUP,
})

This route uses module TTL and prefix, decorator namespace, and a required list group. @Cacheable({ namespace: 'products', ttlSeconds: '5m', evictionGroupName: 'products:list' }) overrides module TTL for that handler only.

set / remember only consume TTL fields from the module (ttlSeconds, expiryUnit). Key-building fields (prefix, namespace, …) apply to the interceptor, not to a hand-built key string.

HTTP caching — @Cacheable + CacheInterceptor

Always pair them on the same handler.

@Controller('products')
export class ProductController {
  @Get()
  @UseInterceptors(CacheInterceptor)
  @Cacheable({
    namespace: 'products',
    ttlSeconds: '5m',
    evictionGroupName: PRODUCTS_LIST_EVICTION_GROUP,
  })
  findAll() {
    return this.productService.findAll();
  }

  @Get('featured')
  @UseInterceptors(CacheInterceptor)
  @Cacheable({
    namespace: 'products',
    key: 'featured',
    ttlSeconds: DEFAULT_CACHE_TTL_SECONDS.medium,
    ignoreQuery: ['utm_source', 'page'],
    varyByUser: true,
    varyHeaders: ['accept-language'],
    evictionGroupName: PRODUCTS_LIST_EVICTION_GROUP,
  })
  findFeatured() {
    return this.productService.findFeatured();
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.productService.findOne(id); // remember() inside the service
  }
}

Hit: handler is not called. Miss: handler runs; concatMap waits for Redis SET + tag index, then emits the same body. Redis errors in that write are swallowed so the HTTP response still succeeds.

Scenarios

  1. GET /products then GET /products — second request is a hit (same key).
  2. GET /products?sort=name vs GET /products?sort=price — different keys, same list group. evict('products:list') clears both.
  3. ?a=1&b=2 vs ?b=2&a=1 — same key (query is sorted).
  4. Cache-buster query keys (_, t, timestamp, cb, nocache, …) are ignored by default.
  5. Empty catalog [] — 200 with [], no Redis write, next GET hits the database again.
  6. After POST — list group gone; next GET all is a miss and rebuilds keys + tag.

Do not GET tag keys with RedisService.get(). Tags are Redis sets. GET on a set is WRONGTYPE. Use sMembers / ttl / TYPE, or SCAN instead of KEYS in production.

Service caching — remember, set, evict

Omit ttlSeconds to inherit the module default. Pass ttlSeconds on the call to override it. evictionGroupName is always required.

@Injectable()
export class ProductService {
  constructor(
    @InjectRepository(Product) private readonly products: Repository<Product>,
    private readonly cacheService: CacheService,
  ) {}

  findOne(id: number) {
    return this.cacheService.remember<Product>({
      key: this.itemKey(id),
      evictionGroupName: productsItemEvictionGroup(id),
      callback: async () => {
        const product = await this.products.findOne({ where: { id } });
        if (!product) throw new NotFoundException();
        return product;
      },
    });
  }

  async create(dto: CreateProductDto) {
    const product = await this.products.save(this.products.create(dto));
    await this.cacheService.evict(PRODUCTS_LIST_EVICTION_GROUP);
    return product;
  }

  async update(id: number, dto: UpdateProductDto) {
    const existing = await this.products.findOne({ where: { id } });
    if (!existing) throw new NotFoundException();
    const product = await this.products.save(
      this.products.merge(existing, dto),
    );
    await this.cacheService.evict(PRODUCTS_LIST_EVICTION_GROUP);
    await this.cacheService.set({
      key: this.itemKey(id),
      value: product,
      evictionGroupName: productsItemEvictionGroup(id),
    });
    return product;
  }

  async remove(id: number) {
    const result = await this.products.delete(id);
    if (!result.affected) throw new NotFoundException();
    await this.cacheService.evict([
      PRODUCTS_LIST_EVICTION_GROUP,
      productsItemEvictionGroup(id),
    ]);
  }

  private itemKey(id: number) {
    return this.cacheService.buildKey({
      namespace: 'products',
      key: ['item', id],
    });
  }
}

Order on PATCH: evict(list) before set(item). Evicting the item group in the same mutation would delete the key you just wrote.

Scenarios

  1. GET one miss → loader → SET + tag TTL copied from the key.
  2. GET one hit → no loader, no re-index.
  3. GET one 404 → exception, nothing stored.
  4. PATCH without a prior GET — set still creates the item key and tag; the next GET one is a hit.
  5. PATCH after GET one — same group; evictionIndex skips SADD if already a member but syncs tag TTL to the new item TTL.
  6. DELETE — both list keys (via list tag members) and the item key disappear.
  7. evict when the tag does not exist — no-op, no throw.
  8. Two list URLs in one tag — one POST clears both HTTP keys.

buildKey

Use cacheService.buildKey() for service keys. HTTP keys come from the interceptor.

this.cacheService.buildKey({ namespace: 'test', key: id });
// cache:test:abc123

this.cacheService.buildKey({
  namespace: 'users',
  key: 'list',
  params: { status: 'active', page: 1 },
  vary: { userId: 42 },
});
// cache:users:list?page=1&status=active#userId=42

Behaviour: sorted params, default ignore-query on HTTP keys, stable nested serialize, path normalize, sanitize, hash suffix when over maxLength (default 250), optional hash: true.

TTL presets

import { DEFAULT_CACHE_TTL_SECONDS } from 'redora';

| Preset | Seconds | |---|---| | instant | 5 | | brief | 15 | | vshort | 60 | | short | 300 | | medium | 600 | | long | 1800 | | vlong / hour | 3600 | | hours2 | 7200 | | hours6 | 21600 | | hours12 | 43200 | | day | 86400 | | days2 | 172800 | | days3 | 259200 | | week | 604800 | | days14 | 1209600 | | month | 2592000 |

There is no forever preset. Persist a key by omitting ttlSeconds on RedisService.set. CacheService.set still requires a group name even when you omit TTL (tag is PERSISTed if the member has no expiry).

Cache API reference

set(options) — write if meaningful, then evictionIndex. Returns Redis 'OK' or null.

get<T>(key) — typed value or null.

remember<T>(options) — miss → callback → set if meaningful.

evict(group | groups[])SMEMBERS each tag, DEL members, DEL tags.

evictionIndex({ groupName, key })SADD if needed, then align tag TTL to the member (EXPIRE extend, or PERSIST if the member has no TTL). Empty groupName throws CacheEvictionException.

delete(key | keys[]) — raw key delete; returns whether any key was removed.

buildKey(options) — deterministic key string.

Inject CACHE_OPTIONS if you need the module defaults in a provider.


Tribe 4 — Observability

ObservabilityModule needs RedisModule. It reads Redis INFO and PING. It does not start a Prometheus scrape endpoint or an OTLP exporter by itself. telemetry() returns a JSON description of backends, filtered INFO fields, and metric rules you can feed into your own instrumentation.

Register

ObservabilityModule.forRoot({
  healthCheck: {
    memory: { show: true, isDetailed: false },
    client: { show: true },
    server: { show: false },
    diagnosis: true,
  },
  telemetry: {
    enabled: true,
    backends: ['prometheus', 'opentelemetry'],
    prometheus: {
      enabled: true,
      endpoint: '/metrics',
      namespace: 'redora',
      prefix: 'redis_',
      defaultLabels: { env: 'prod' },
    },
    openTelemetry: {
      enabled: true,
      serviceName: 'orders-api',
      serviceVersion: '1.2.0',
      exporterEndpoint: 'http://otel-collector:4318',
      temporality: 'cumulative',
      resourceAttributes: { 'deployment.environment': 'prod' },
    },
    sources: {
      memory: { enabled: true, detailed: false, fields: ['used_memory'] },
      diagnosis: { enabled: true, groups: ['keyspace', 'eventloop'] },
    },
    metrics: [
      {
        name: 'redis_memory_usage',
        source: 'memory',
        path: 'used_memory',
        format: 'gauge',
        unit: 'bytes',
      },
    ],
  },
});

Per-call options on healthCheck(options) / telemetry(options) override the module defaults for that request.

Health endpoint

@Controller('redis')
export class RedisHealthController {
  constructor(private readonly observability: ObservabilityService) {}

  @Get('health')
  health() {
    return this.observability.healthCheck();
  }
}

healthCheck():

  1. PING — if not PONG, throws RedisUnhealthyException (HTTP 500).
  2. Optionally attaches memory, client, server (summary or isDetailed full INFO parse).
  3. Optionally attaches diagnosis (parsed INFO stats grouped: connections, commands, network, replication, expiration, eviction, keyspace, pubsub, …).
await this.observability.healthCheck({
  memory: { show: true, isDetailed: true },
  client: { show: true },
  server: { show: true },
  diagnosis: true,
});

Direct helpers: memory(detailed?), client(detailed?), server(detailed?), diagnosis().

REDIS_INFO_KEYS (memory, clients, server, stats) is exported if you call redisService.info(REDIS_INFO_KEYS.MEMORY) yourself.

Telemetry endpoint

@Get('telemetry')
telemetry() {
  return this.observability.telemetry({
    backends: ['opentelemetry'],
    openTelemetry: { enabled: true, serviceName: 'api' },
    sources: {
      memory: { enabled: true, fields: ['used_memory'] },
      client: { enabled: false },
      diagnosis: { enabled: true, groups: ['keyspace'] },
    },
    metrics: [
      {
        name: 'redis_memory_usage',
        source: 'memory',
        path: 'used_memory',
        format: 'histogram',
        unit: 'bytes',
        labels: { server: 'cache-1' },
      },
      {
        name: 'redis_keyspace_hits',
        source: 'diagnosis',
        path: 'keyspace.keyspace_hits',
        format: 'counter',
      },
    ],
  });
}
  • enabled: false{ enabled: false } and no INFO calls for that invocation’s default path.
  • Sources default to on unless enabled: false.
  • fields / groups pick a subset; omit them to return the full snapshot for that source.
  • metrics[].path is a dotted path into the collected source object.
  • Prometheus / OpenTelemetry blocks in the JSON are config echoes (endpoint, namespace, labels). Wire prom-client or the OTel SDK separately using those values.

Scenarios

  1. Liveness: healthCheck() with no extras — ping only.
  2. Ops dashboard: memory + diagnosis.
  3. Deep incident: isDetailed: true on memory/client/server.
  4. Metrics pipeline: telemetry() with metrics rules; push value into your counters.
  5. Unhealthy Redis: ping fails → RedisUnhealthyException.

Exceptions

| Class | When | |---|---| | TimeFormatExceptions | Invalid duration string, expiryUnit mixed with a string TTL, non-positive numeric TTL | | CacheEvictionException | Empty eviction group name | | RedisUnhealthyException | PING did not return PONG |

They extend Nest InternalServerErrorException, so default exception filters return HTTP 500 unless you map them.


Public exports (redora)

Modules and services: RedisModule, RedisService, CacheModule, CacheService, CacheInterceptor, Cacheable, RedisLoggerModule, RedisLoggerService, ObservabilityModule, ObservabilityService.

Tokens: REDIS_CLIENT, CACHE_OPTIONS, REDIS_LOGGER_OPTIONS, OBSERVABILITY_OPTIONS, REDIS_INFO_KEYS.

Options / types: RedisConnectionOptions, RedisConnectionAsyncOptions, SetOptions, ZMember, CacheOptions, CacheKeyOptions, CacheSetOptions, CacheRememberOptions, EvictionIndexOptions, ExpiryUnits, DEFAULT_CACHE_TTL_SECONDS, DEFAULT_CACHE_KEY, RedisLoggerOptions, ObservabilityOptions, HealthCheckOptions, TelemetryOptions (and nested telemetry types), MemoryInfo, ClientInfo, ServerInfo, RedisDiagnosis.

Exceptions: TimeFormatExceptions, CacheEvictionException, RedisUnhealthyException.


What's in 0.3.0

  • Redis command mixins, serialization, optional connection, logger
  • Cache interceptor (concatMap), remember / set / evict / tag TTL sync
  • Meaningful-data skip on interceptor, remember, and set
  • Observability health + telemetry JSON
  • Duration-string TTLs; numeric TTL defaults to seconds

Roadmap

  • Queues (BullMQ), rate limiting, distributed locking, Redis for AI
  • Sliding expiration
  • Built-in Prometheus / OTLP exporters (today: structured payloads only)

License

MIT