Horizon Framework

Cache Factory

CacheFactory provides a flexible caching mechanism to improve performance by storing and reusing the results of expensive operations. It supports both in-memory and persisted (IndexedDB) caching with automatic expiration.

Quick Start

import { CacheFactory } from '@skedulo/horizon-core'

// Create a cache with persistence enabled (default)
const myCache = CacheFactory.create<RequestParams, ResponseData>(
  'my-cache-name',
  true, // enablePersistence
  3600000, // TTL in milliseconds (1 hour, default)
)

// Use the cache
const data = await myCache.execute(
  { id: '123' }, // cache key
  async () => {
    // Expensive operation
    return await fetchData('123')
  },
)

Cache Types

The factory can create in memory or persisted cache:

1. PersistedCache (Default)

Stores data in IndexedDB for persistence across browser sessions.

const persistedCache = CacheFactory.create<Params, Data>(
  'my-persisted-cache',
  true, // persistence enabled
  3600000, // 1 hour TTL
)

Features:

  • Survives browser refreshes and restarts
  • Uses IndexedDB via idb-keyval
  • Automatic cleanup of expired entries on load
  • Falls back to memory cache if IndexedDB fails

2. LocalCache

In-memory only cache that doesn't persist across sessions.

const localCache = CacheFactory.create<Params, Data>(
  'my-local-cache',
  false, // persistence disabled
  1800000, // 30 minutes TTL
)

Features:

  • Fast access (no disk I/O)
  • Cleared when page reloads
  • Useful for temporary data or when persistence isn't needed

Configuration

Caching behavior can be controlled via localStorage:

Enable/Disable Caching

// Disable all caches
localStorage.setItem('HORIZON_CORE_ENABLED_CACHES', 'none')

// Enable all caches (default)
localStorage.setItem('HORIZON_CORE_ENABLED_CACHES', 'all')

// Enable only local (non-persisted) caches
localStorage.setItem('HORIZON_CORE_ENABLED_CACHES', 'local')

// Enable specific caches by name
localStorage.setItem('HORIZON_CORE_ENABLED_CACHES', 'cache-name-1,cache-name-2')

Cache Interface

All cache types implement the same interface:

interface Cache<R, T> {
  execute: (key: R, fn: () => Promise<T>) => Promise<T>
  invalidate: (key: R) => void
}

execute()

Executes a function with caching. If a valid cached value exists, it returns immediately. Otherwise, executes the function and caches the result.

const result = await cache.execute(
  cacheKey, // Serializable cache key (will be JSON.stringified)
  async () => {
    // Your expensive operation
    return await apiCall()
  },
)

Parameters:

  • key: R - Any serializable value used as the cache key
  • fn: () => Promise<T> - The function to execute if cache miss

Returns: Promise<T> - The cached or newly computed result

invalidate()

Removes a specific entry from the cache.

cache.invalidate({ id: '123' })

Parameters:

  • key: R - The cache key to invalidate

Time-To-Live (TTL)

Cached entries automatically expire after their TTL. The default is 1 hour (3,600,000 ms).

// Custom TTL examples
const shortLivedCache = CacheFactory.create('short', true, 60000) // 1 minute
const longLivedCache = CacheFactory.create('long', true, 86400000) // 24 hours

When an entry expires:

  • It's automatically removed from the cache
  • The next execute() call will run the provided function
  • For persisted caches, expired entries are cleaned up on page load

Best Practices

1. Use Unique Cache Names

Each cache must have a unique name. Duplicate names will throw an error:

const cache1 = CacheFactory.create('my-cache', true)
const cache2 = CacheFactory.create('my-cache', true) // ❌ Error!

2. Choose Appropriate TTL

Set TTL based on how frequently your data changes:

// Fast-changing data: short TTL
const userStatusCache = CacheFactory.create('user-status', true, 60000) // 1 min

// Slow-changing data: long TTL
const configCache = CacheFactory.create('config', true, 3600000) // 1 hour

// Static data: very long TTL
const metadataCache = CacheFactory.create('metadata', true, 86400000) // 24 hours

3. Use Serializable Cache Keys

Cache keys are serialized using JSON.stringify(), so use simple objects:

// ✅ Good cache keys
cache.execute({ id: '123' }, ...)
cache.execute({ userId: 'abc', type: 'profile' }, ...)
cache.execute('simple-string', ...)

// ❌ Avoid functions, symbols, or circular references
cache.execute({ fn: () => {} }, ...) // Won't work as expected

4. Disable Persistence for Sensitive Data

For security-sensitive data, use in-memory caching only:

const authCache = CacheFactory.create(
  'auth-tokens',
  false, // Don't persist to disk
  300000, // 5 minutes
)

5. Invalidate on Updates

Manually invalidate cache entries when underlying data changes:

// After updating data
await updateUser(userId, newData)
userCache.invalidate({ id: userId })

Real-World Example

import { CacheFactory } from '@skedulo/horizon-core'

// Create a cache for API responses
const apiCache = CacheFactory.create<
  { endpoint: string; params?: Record<string, string> },
  any
>(
  'api-responses',
  true, // Persist across sessions
  1800000, // 30 minutes
)

// Usage in a data fetching function
async function fetchUserProfile(userId: string) {
  return apiCache.execute(
    { endpoint: 'users', params: { id: userId } },
    async () => {
      console.log('Cache miss - fetching from API')
      const response = await fetch(`/api/users/${userId}`)
      return response.json()
    },
  )
}

// First call: fetches from API
const profile1 = await fetchUserProfile('123')

// Second call: returns from cache
const profile2 = await fetchUserProfile('123')

// After updating the user
await updateUser('123', { name: 'New Name' })
apiCache.invalidate({ endpoint: 'users', params: { id: '123' } })

// Next call: fetches fresh data
const profile3 = await fetchUserProfile('123')