A dependency-free, lightweight runtime caching library for JavaScript and TypeScript applications. RunCache allows you to cache string values with configurable time-to-live (TTL) settings and supports automatic value regeneration through source functions.
- Zero Dependencies: Lightweight implementation with no external dependencies
- In-Memory Performance: Fast, efficient runtime cache for optimal application performance
- Source Function Support: Cache values generated from synchronous or asynchronous functions
- Automatic Refetching: Configure cache entries to automatically refresh on expiration
- Comprehensive Event System: Subscribe to cache events including expiry, refetch, and refetch failures
- Pattern Matching: Use wildcard patterns to operate on groups of related cache keys
- Eviction Policies: Configure size limits with LRU (Least Recently Used) or LFU (Least Frequently Used) eviction strategies
- Middleware Support: Add custom processing for intercepting and transforming cache operations
- Tag-based Invalidation: Group related cache entries with tags for efficient batch invalidation
- Dependency Tracking: Establish relationships between cache entries with automatic cascading invalidation
- TypeScript Support: Full type definitions included
npm install run-cache
import { RunCache } from "run-cache";
// Basic caching
await RunCache.set({ key: "user-profile", value: JSON.stringify({ name: "John Doe" }) });
const profile = await RunCache.get("user-profile");
// Cache with expiration (TTL in milliseconds)
await RunCache.set({
key: "api-data",
value: JSON.stringify({ data: [1, 2, 3] }),
ttl: 60000 // 1 minute
});
// Cache with automatic refresh
await RunCache.set({
key: "weather-data",
sourceFn: async () => JSON.stringify(await fetchWeatherData()),
ttl: 300000, // 5 minutes
autoRefetch: true
});
// Using wildcard patterns
await RunCache.set({ key: "user-1", value: "Alice" });
await RunCache.set({ key: "user-2", value: "Bob" });
const users = await RunCache.get("user-*"); // Returns array of all matching values
RunCache supports configurable cache eviction policies to manage memory usage by automatically removing entries when the cache reaches a defined size limit.
- NONE: No automatic eviction (default). Cache entries are only removed via TTL or manual deletion.
- LRU (Least Recently Used): Removes the least recently accessed entries when the cache exceeds its maximum size.
- LFU (Least Frequently Used): Removes the least frequently accessed entries when the cache exceeds its maximum size. When entries have the same access frequency, the oldest entry is removed first.
import { RunCache, EvictionPolicy } from "run-cache";
// Configure the cache with a max size of 100 entries and LRU eviction policy
RunCache.configure({
maxEntries: 100,
evictionPolicy: EvictionPolicy.LRU
});
// Add entries to the cache
for (let i = 0; i < 150; i++) {
await RunCache.set({ key: `item-${i}`, value: `value-${i}` });
}
// Only the 100 most recently used entries will be retained
// The oldest 50 entries will be automatically evicted
// Change to LFU eviction policy
RunCache.configure({
evictionPolicy: EvictionPolicy.LFU
});
// Get the current configuration
const config = RunCache.getConfig();
console.log(config); // { maxEntries: 100, evictionPolicy: "lfu" }
RunCache supports tagging cache entries and invalidating groups of related entries using tags.
// Set entries with tags
await RunCache.set({
key: "user:123:profile",
value: JSON.stringify({ name: "John Doe" }),
tags: ["user:123", "profile"]
});
await RunCache.set({
key: "user:123:settings",
value: JSON.stringify({ theme: "dark" }),
tags: ["user:123", "settings"]
});
await RunCache.set({
key: "user:123:posts",
value: JSON.stringify([{ id: 1, title: "Hello" }]),
tags: ["user:123", "posts"]
});
// Later, when user data changes, invalidate all related cache entries with a single call
RunCache.invalidateByTag("user:123");
// This removes all three entries with the "user:123" tag
// You can also listen for tag invalidation events
RunCache.onTagInvalidation((event) => {
console.log(`Cache entry ${event.key} was invalidated by tag: ${event.tag}`);
});
RunCache enables establishing dependency relationships between cache entries with automatic cascading invalidation.
// Set the primary data cache
await RunCache.set({
key: "user:123:profile",
value: JSON.stringify({ name: "John Doe" })
});
// Set dependent caches that rely on the profile data
await RunCache.set({
key: "user:123:dashboard",
value: JSON.stringify({ widgets: [...] }),
dependencies: ["user:123:profile"]
});
await RunCache.set({
key: "user:123:recommendations",
value: JSON.stringify([...]),
dependencies: ["user:123:profile"]
});
// Create multi-level dependencies
await RunCache.set({
key: "home:feed",
value: JSON.stringify([...]),
dependencies: ["user:123:recommendations"]
});
// Check if one entry depends on another (directly or indirectly)
const isDependency = await RunCache.isDependencyOf("home:feed", "user:123:profile");
console.log(isDependency); // true
// When the primary data changes, invalidate dependent entries with cascading effect
RunCache.invalidateByDependency("user:123:profile");
// This will invalidate all dependent entries, including nested dependencies:
// - user:123:dashboard
// - user:123:recommendations
// - home:feed (because it depends on user:123:recommendations)
// You can also listen for dependency invalidation events
RunCache.onDependencyInvalidation((event) => {
console.log(`Cache entry ${event.key} was invalidated due to dependency on: ${event.dependencyKey}`);
});
await RunCache.set({
key: string, // Required: Unique identifier for the cache entry
value?: string, // Optional: String value to cache (required if no sourceFn)
ttl?: number, // Optional: Time-to-live in milliseconds
autoRefetch?: boolean, // Optional: Automatically refetch on expiry (requires ttl and sourceFn)
sourceFn?: () => string | Promise<string>, // Optional: Function to generate cache value (required if no value)
tags?: string[], // Optional: Array of tags for tag-based invalidation
dependencies?: string[] // Optional: Array of cache keys this entry depends on
});
// Get a single cache entry
const value = await RunCache.get("cache-key");
// Get multiple entries using wildcards (returns an array)
const values = await RunCache.get("user-*");
// Manually refresh a cache entry (requires a sourceFn)
await RunCache.refetch("cache-key");
// Refresh multiple entries using wildcards
await RunCache.refetch("api-data-*");
// Remove a specific entry
RunCache.delete("cache-key");
// Remove multiple entries using wildcards
RunCache.delete("temp-*");
// Remove all cache entries
RunCache.flush();
// Invalidate all entries with a specific tag
RunCache.invalidateByTag("user:123");
// Invalidate all entries dependent on a specific key
RunCache.invalidateByDependency("user:123:profile");
// Check if one key depends on another
const isDependency = await RunCache.isDependencyOf("dashboard:123", "user:123");
// Check if a valid (non-expired) cache entry exists
const exists = await RunCache.has("cache-key");
// Check if any matching cache entries exist
const hasItems = await RunCache.has("session-*");
// Configure cache settings
RunCache.configure({
maxEntries: 1000, // Maximum number of entries before eviction
evictionPolicy: EvictionPolicy.LRU // Eviction policy to use
});
// Get current configuration
const config = RunCache.getConfig();
RunCache provides a comprehensive event system to monitor cache lifecycle events.
// Global expiry event
RunCache.onExpiry((event) => {
console.log(`Cache key ${event.key} expired at ${new Date(event.updatedAt + event.ttl).toISOString()}`);
});
// Specific key expiry
RunCache.onKeyExpiry("api-data", (event) => {
console.log(`API data cache expired`);
});
// Pattern-based expiry events
RunCache.onKeyExpiry("user-*", (event) => {
console.log(`User cache ${event.key} expired`);
});
// Global refetch event
RunCache.onRefetch((event) => {
console.log(`Cache key ${event.key} was refreshed`);
});
// Specific key refetch
RunCache.onKeyRefetch("weather-data", (event) => {
console.log(`Weather data was refreshed`);
});
// Pattern-based refetch events
RunCache.onKeyRefetch("stats-*", (event) => {
console.log(`Statistics for ${event.key} were refreshed`);
});
// Global refetch failure event
RunCache.onRefetchFailure((event) => {
console.error(`Failed to refresh cache key ${event.key}`);
});
// Specific key refetch failure
RunCache.onKeyRefetchFailure("api-data", (event) => {
console.error(`API data refresh failed`);
});
// Pattern-based refetch failure events
RunCache.onKeyRefetchFailure("external-*", (event) => {
console.error(`External data refresh failed for ${event.key}`);
});
// Global tag invalidation event
RunCache.onTagInvalidation((event) => {
console.log(`Cache key ${event.key} was invalidated by tag: ${event.tag}`);
});
// Specific key tag invalidation
RunCache.onKeyTagInvalidation("user:profile:*", (event) => {
console.log(`User profile cache was invalidated by tag: ${event.tag}`);
});
// Global dependency invalidation event
RunCache.onDependencyInvalidation((event) => {
console.log(`Cache key ${event.key} was invalidated due to dependency on: ${event.dependencyKey}`);
});
// Specific key dependency invalidation
RunCache.onKeyDependencyInvalidation("dashboard:*", (event) => {
console.log(`Dashboard cache was invalidated due to dependency on: ${event.dependencyKey}`);
});
// Remove all event listeners
RunCache.clearEventListeners();
// Remove listeners for a specific event type
RunCache.clearEventListeners({
event: EVENT.EXPIRE
});
// Remove listeners for a specific key
RunCache.clearEventListeners({
event: EVENT.REFETCH,
key: "api-data"
});
// Remove listeners using wildcard patterns
RunCache.clearEventListeners({
event: EVENT.REFETCH_FAILURE,
key: "external-*"
});
RunCache supports middleware functions that can intercept and transform cache operations. This allows for powerful customizations like validation, normalization, encryption, or custom business logic.
import { RunCache } from "run-cache";
// Add a simple logging middleware
RunCache.use(async (value, context, next) => {
console.log(`${context.operation} operation for key: ${context.key}`);
return next(value);
});
import { RunCache } from "run-cache";
import { encrypt, decrypt } from "./your-crypto-lib";
// Add encryption middleware
RunCache.use(async (value, context, next) => {
if (context.operation === 'set' && value) {
// Encrypt value before storing in cache
return next(encrypt(value));
} else if (context.operation === 'get' || context.operation === 'refetch') {
// Decrypt value after retrieving from cache
const encrypted = await next(value);
return encrypted ? decrypt(encrypted) : undefined;
}
return next(value);
});
// Use cache normally - encryption/decryption happens transparently
await RunCache.set({ key: "sensitive-data", value: "secret-value" });
const value = await RunCache.get("sensitive-data"); // Returns decrypted "secret-value"
Middleware functions are executed in the order they are added:
// First middleware validates input
RunCache.use(async (value, context, next) => {
if (context.operation === 'set' && (!value || value.length < 3)) {
throw new Error("Value must be at least 3 characters");
}
return next(value);
});
// Second middleware adds a timestamp
RunCache.use(async (value, context, next) => {
if (context.operation === 'set' && value) {
return next(`${value}|${Date.now()}`);
} else if (context.operation === 'get' && value) {
const result = await next(value);
return result?.split('|')[0]; // Remove timestamp on get
}
return next(value);
});
// Remove all middleware
RunCache.clearMiddleware();
For more detailed information on middleware, see the middleware documentation.
RunCache supports wildcard pattern matching for operating on multiple related cache keys simultaneously. Use the *
character as a wildcard in your key patterns.
// Cache multiple related entries
await RunCache.set({ key: "user:1:profile", value: "Alice's data" });
await RunCache.set({ key: "user:2:profile", value: "Bob's data" });
await RunCache.set({ key: "user:1:preferences", value: "Alice's preferences" });
await RunCache.set({ key: "user:2:preferences", value: "Bob's preferences" });
// Get all user profiles
const profiles = await RunCache.get("user:*:profile");
// Get all data for user 1
const user1Data = await RunCache.get("user:1:*");
// Delete all preference data
RunCache.delete("user:*:preferences");
// Check if any user 2 data exists
const hasUser2Data = await RunCache.has("user:2:*");
// Refresh all profile data
await RunCache.refetch("user:*:profile");
Wildcard support is implemented for all key-based operations:
get
: Returns an array of values for matching keysdelete
: Removes all matching keysrefetch
: Refreshes all matching keyshas
: Returns true if any matching key exists- Event listeners: Registers callbacks for keys matching patterns
clearEventListeners
: Removes listeners for keys matching patterns
RunCache is designed to properly manage resources throughout the application lifecycle.
RunCache automatically registers handlers for SIGTERM and SIGINT signals in Node.js environments to ensure proper cleanup of all resources when the application is shutting down. This prevents memory leaks and ensures a clean shutdown.
You can also manually trigger a complete shutdown of the cache:
// Manually shut down the cache, clearing all entries, intervals, and event listeners
RunCache.shutdown();
The shutdown
method performs the following cleanup:
- Clears all cache entries and their associated intervals
- Removes all event listeners
- Resets the cache configuration to default values
This is particularly useful in long-running applications or when you need to release resources manually.
RunCache provides built-in debug logging to help with debugging and monitoring cache behavior.
Enable debug logging to see detailed information about all cache operations:
// Enable debug logging when configuring the cache
RunCache.configure({
debug: true
});
When debug mode is enabled, RunCache logs detailed information about:
- Cache operations (set, get, delete, etc.)
- Entry expiration and eviction
- Refetch operations and failures
- Configuration changes
All logs include timestamps and log levels for easy filtering.
// Example of enabling debug logging for debugging
RunCache.configure({ debug: true });
// Perform some cache operations
await RunCache.set({ key: "user-1", value: "Alice" });
await RunCache.get("user-1");
// Disable debug logging when done debugging
RunCache.configure({ debug: false });
Logs are output at different levels depending on their importance:
info
: General information about cache operationsdebug
: Detailed information useful for debuggingwarn
: Warnings about potential issueserror
: Errors that occurred during operations
This is useful when you want to filter logs in complex applications.
RunCache now supports persisting cache data across application restarts using storage adapters. This allows you to maintain your cache state between sessions, improving user experience and reducing unnecessary API calls.
RunCache includes three built-in storage adapters:
- LocalStorageAdapter: For browser environments, uses the browser's localStorage API
- IndexedDBAdapter: For browser environments, uses the browser's IndexedDB API for larger datasets
- FilesystemAdapter: For Node.js environments, stores cache data in the filesystem
To use a storage adapter, first import the adapter you want to use, then configure RunCache with it:
import { RunCache, LocalStorageAdapter } from 'run-cache';
// Configure RunCache with LocalStorage persistence
RunCache.configure({
storageAdapter: new LocalStorageAdapter({
storageKey: 'my-app-cache' // Optional custom key
})
});
You can configure automatic saving at regular intervals:
// Save cache to storage every 5 minutes (300,000 ms)
RunCache.setupAutoSave(300000);
// Disable auto-saving
RunCache.setupAutoSave(0);
You can also manually control when to save and load:
// Manually save cache state
await RunCache.saveToStorage();
// Manually load cache state
await RunCache.loadFromStorage();
Each adapter accepts the following common options:
interface StorageAdapterConfig {
// Storage key/filename to use
storageKey?: string; // Default: "run-cache-data"
// Auto-save interval in milliseconds
autoSaveInterval?: number; // Default: 0 (disabled)
// Whether to load cache automatically when adapter is initialized
autoLoadOnInit?: boolean; // Default: true
}
The FilesystemAdapter also accepts an additional option:
// Custom file path (FilesystemAdapter only)
new FilesystemAdapter({
filePath: '/custom/path/to/cache.json'
});
When the application restarts, the cache state is automatically restored if:
- A storage adapter is configured when initializing RunCache
- The adapter contains valid cached data
This recovery happens automatically when you configure RunCache with a storage adapter. Any cache entries with TTL values will have their expiry timers properly restored based on their original expiration time.
import { RunCache, LocalStorageAdapter } from 'run-cache';
// Configure RunCache with persistence
RunCache.configure({
maxEntries: 1000,
evictionPolicy: EvictionPolicy.LRU,
storageAdapter: new LocalStorageAdapter({
storageKey: 'my-app-cache'
})
});
// Set up auto-save every minute
RunCache.setupAutoSave(60000);
// When the app shuts down, the cache will be saved automatically
// When the app starts up, the cache will be loaded automatically
This project is licensed under the MIT License - see the LICENSE file for details.