Redis-based cache implementation with graceful degradation

Implements both CacheClient (generic key-value) and LookupCache (converter-specific).

All cache keys are prefixed with jml: to avoid conflicts. Cache operations fail gracefully - errors are logged but don't crash the library. Only the ping() method throws errors.

Implements

  • CacheClient
  • LookupCache

Constructors

  • Creates a new Redis cache instance

    Parameters

    • config: RedisConfig

      Redis connection configuration

    • Optional redisInstance: Redis

      Optional Redis instance for testing (uses ioredis if not provided)

    • logger: Logger = defaultLogger

      Optional logger for dependency injection (defaults to console)

    • debug: boolean = false

      Optional debug flag to enable verbose cache logging (defaults to false)

    Returns RedisCache

Methods

  • Clear all keys matching the jml: prefix

    Uses SCAN command to avoid blocking Redis with large datasets

    Returns Promise<void>

  • Clear lookup caches

    Parameters

    • projectKey: string

      JIRA project key

    • Optional fieldType: string

      Optional field type (clears all if omitted)

    • Optional issueType: string

      Optional issue type

    Returns Promise<void>

    Example

    // Clear all lookups for project
    await cache.clearLookups('TEST');

    // Clear specific lookup type
    await cache.clearLookups('TEST', 'priority');

    // Clear issuetype-specific lookup
    await cache.clearLookups('TEST', 'component', 'Story');
  • Delete a key from the cache

    Parameters

    • key: string

      Cache key (will be prefixed with jml:)

    Returns Promise<void>

  • Disconnect from Redis and clean up event listeners

    Returns Promise<void>

  • Get a value from the cache with stale-while-revalidate support

    Returns both the value and whether it's stale. By default, stale values are returned - callers should trigger background refresh when isStale=true.

    Parameters

    • key: string

      Cache key (will be prefixed with jml:)

    • Optional options: {
          rejectStale?: boolean;
      }
      • Optional rejectStale?: boolean

        If true, returns null for stale values (bypasses SWR)

    Returns Promise<{
        isStale: boolean;
        value: null | string;
    }>

    CacheResult with value and isStale flag

  • Retrieve cached lookup values

    Uses CacheClient.get() for consistent error handling and key prefixing.

    Parameters

    • projectKey: string

      JIRA project key

    • fieldType: string

      Type of lookup field

    • Optional issueType: string

      Optional issue type for issuetype-specific lookups

    Returns Promise<{
        isStale: boolean;
        value: null | unknown[];
    }>

    Object with value (array or null) and isStale flag

    Example

    const { value, isStale } = await cache.getLookup('TEST', 'priority');
    const { value, isStale } = await cache.getLookup('TEST', 'component', 'Story');
  • Check if a refresh is currently in progress for a key

    Useful for testing and debugging.

    Parameters

    • key: string

      The refresh key to check

    Returns boolean

    True if refresh is in progress

  • Test connection to Redis

    Returns Promise<void>

    Throws

    if connection fails

  • Execute a refresh function with deduplication.

    If a refresh is already in progress for this key, returns the existing promise instead of starting a new one. This prevents duplicate API calls when multiple stale cache hits occur for the same data.

    Parameters

    • key: string

      Unique key identifying this refresh operation (e.g., cache key)

    • refreshFn: (() => Promise<void>)

      Async function that fetches fresh data and updates cache

        • (): Promise<void>
        • Returns Promise<void>

    Returns Promise<void>

    Promise that resolves when refresh is complete

    Example

    // Multiple concurrent calls to this will only trigger ONE API call
    await cache.refreshOnce('lookup:TEST:users', async () => {
    const users = await fetchUsersFromJira();
    await cache.setLookup('TEST', 'users', users);
    });
  • Set a value in the cache with soft TTL (supports stale-while-revalidate)

    Stores value with soft expiry timestamp. After soft TTL, value is "stale" but still returned - callers should trigger background refresh. Hard TTL is 24 hours just to prevent infinite Redis growth.

    Parameters

    • key: string

      Cache key (will be prefixed with jml:)

    • value: string

      Value to store

    • ttlSeconds: number

      Soft TTL - value is "stale" after this but still usable

    Returns Promise<void>

  • Cache lookup values (priorities, components, versions, etc.)

    Uses CacheClient.set() for consistent error handling and key prefixing. Cache key pattern: lookup:{projectKey}:{fieldType}:{issueType?}

    Parameters

    • projectKey: string

      JIRA project key (e.g., "TEST")

    • fieldType: string

      Type of lookup field (e.g., "priority", "component", "version")

    • data: unknown[]

      Array of lookup values to cache

    • Optional issueType: string

      Optional issue type for issuetype-specific lookups

    Returns Promise<void>

    Example

    // Cache priority list (project-level)
    await cache.setLookup('TEST', 'priority', [
    { id: '1', name: 'Blocker' },
    { id: '2', name: 'High' }
    ]);

    // Cache components (issuetype-specific)
    await cache.setLookup('TEST', 'component', [...], 'Story');