cache-flow
    Preparing search index...

    cache-flow

    Cache Flow

    Version License: ISC master CI Status master Coverage Status GitHub last commit NPM Downloads

    Allows to create configurable caches for NodeJS applications with automatic cache loading. Internal caching is supported by an in-memory cache or Redis, with automatic fallback in case of Redis disconnection.

    Cache Flow also provides a @Cacheable decorator for Typescript projects (inspired from Java/Spring), packing all the caching features of this library into a single line of code, completely transparently for method callers.

    Table of Contents

    Requirements: Node.js 22 or later. The library is tested against Node.js 22 (Jod) and 24 (Krypton), the currently supported LTS release lines.

    1. Install the library
    npm install --save cache-flow
    
    1. reflect-metadata shim is required:

    npm install --save reflect-metadata

    Make sure to import it before you use cache-flow (best is to import it at the top of your index file):

    import 'reflect-metadata';
    
    1. Create a cache SimpleCache.ts

    1.a. Typescript example:

    import { CacheLoader } from 'cache-flow';

    class SimpleCache extends CacheLoader<string, string> {

    constructor() {
    super('simple-cache-1', {
    expirationTime: 3600 // 1h in seconds
    });
    }

    protected async load(key: string): Promise<string> {
    const now = new Date();
    return key + '-' + now.getTime();
    }

    }

    Also see the code example

    1.b. ES6 Javascript example:

    Even though it does not provide all the genericity Cache Flow comes with when used in a Typescript environment, native javascript is also completely supported:

    import { CacheLoader } from 'cache-flow';
    // also works with
    // const { CacheLoader } = require('cache-flow');

    class ES6ExampleCache extends CacheLoader {

    constructor() {
    super('es6-example-cache', {
    expirationTime: 3600 // 1h in seconds
    });
    }


    load(key) {
    const now = new Date();
    return key + '-' + now.getTime();
    }

    }

    Also see the code example

    1. Use your cache
    const cache = new SimpleCache();
    const myValue = await cache.get('myKey');
    setTimeout(async function () {
    const myValue2 = await cache.get('myKey');
    console.log(myValue);
    console.log(myValue2); // myValue2 has the same value as myValue!
    }, 3000);
    1. What happened behind the scenes

    Unlike lots of cache libraries, with Cache Flow, you don't need to manually check if a key exists in your cache before trying to get it, and set the value yourself afterwards.

    When you call the get function of your cache, Cache Flow automatically takes care of calling the load function your previously defined, and stores the value for a later call.

    So if you call get right after, giving the same key, it will bypass the loader function, and directly pull the value from the inner cache.

    class UserProfileCache extends CacheLoader<User, UserProfile> {

    constructor() {
    super('user-profile-cache', {
    expirationTime: 3600 // 1h in seconds
    });
    }

    protected async load(user: User): Promise<UserProfile> {
    const profile = await userProfileService.getProfile(user);
    return profile;
    }

    protected keyToString(user: User): string {
    return user.id;
    }

    }

    Also see the code example

    Cache Flow allows you to define caches with more complex keys and values, which can be objects, arrays, ...

    This way, you can define your own cache key structures:

    class CustomCache extends CacheLoader<MyCacheKey, CachedObject> {

    constructor() {
    super('custom-cache-key', {
    expirationTime: 3600 // 1h in seconds
    });
    }

    protected async load(key: MyCacheKey): Promise<CachedObject> {
    const result = await doSomething(key);
    return result;
    }

    }

    interface MyCacheKey {
    id: string;
    someField: string;
    someOtherField: number;
    }

    A cache defined with such a key will compute a hash of your key objects to use as the internal cache key. You can also define a custom keyToString function, transforming your key into a unique string identifying your key. It can be as simple as an ID, or a combination of several parameters that define your key, like:

    class CustomCache extends CacheLoader<Identifier, string> {

    protected keyToString(identifier: Identifier): string {
    return `${identifier.code}-${identifier.language}`;
    }

    }

    When using Typescript, you have access to a powerful shortcut to caching: you can annotate your class method with @Cacheable. This automatically wraps your method call with a caching layer, meaning when calling your method, Cache Flow will take care of checking for an existing cached value, and otherwise will execute the method to get one. With this, adding a cache is completely transparent for anyone calling this method, without any other required change than annotating your method with the @Cacheable decorator.

    Here is an example:

    class CacheableExampleClass {

    private readonly id: string;

    constructor(id: string) {
    this.id = id;
    }

    @Cacheable()
    public async getResult(prefix: string, value: number): Promise<string> {
    await sleep(100);
    const now = new Date();
    return `${this.id}-${prefix}-${now.getTime()}${now.getMilliseconds()}-${value * 3}`;
    }

    }

    Also see the code example

    @Cacheable provides the same flexibility as manually declaring a class cache: you can configure the cacheId, expirationTime and maxSize parameters, as well as the argsToKey, serialize and deserialize functions (see documentation below).

    By default, @Cacheable will use object-hash on the array of arguments to create a cache key. In cases where you have more complex objects as your cache key, and only want to use a subset of these objects, you can define a custom way of inferring the cache key from arguments:

    @Cacheable({
    argsToKey: (user: User) => {
    return `${user.id}-${user.lastUpdate}`;
    },
    options: {
    expirationTime: 3600
    }
    })

    If argsToKey returns a string, this string is directly used as the cache key. Otherwise, the cache computes a hash of the returned value and uses it as the cache key. Make sure your argsToKey function actually returns a value: one that returns nothing maps every call to the same cache key.

    The cache backing a @Cacheable method is created once per decorated method, when the class is defined, and the cache key is derived from the method arguments only. All instances of the class therefore share the same cache entries, and two instances called with equal arguments get the same cached value even if their internal state differs:

    class Greeter {

    constructor(private readonly name: string) {
    }

    @Cacheable()
    public async greet(greeting: string): Promise<string> {
    return `${greeting}, ${this.name}`;
    }

    }

    await new Greeter('Alice').greet('Hello'); // 'Hello, Alice', loaded
    await new Greeter('Bob').greet('Hello'); // 'Hello, Alice' — same key, served from the cache

    If a method's result depends on instance state, include that state in the cache key with argsToKey, or use a CacheLoader whose key covers it.

    By default, @Cacheable uses an expirationTime of 3600 seconds (1 hour), which differs from the CacheLoader default of 24 hours. Set options.expirationTime explicitly if you rely on a particular value.

    You can find more examples in the examples folder of the source repository: https://github.com/abourdin/cache-flow/tree/master/examples

    Cache Flow also supports distributed caching by using Redis as the caching engine. To have your cache instances use Redis behind the scenes, Cache Flow must be configured as part of the startup of your application, before any cache is instantiated.

    import { CacheFlow } from 'cache-flow';

    CacheFlow.configure({
    redis: {
    host: 'your.redis.server.com',
    port: 6379
    }
    });

    // all set, now you can start caching!
    const cache = new SimpleCache();

    Both host and port are required. configure throws if a redis configuration is given with only one of them, rather than leaving your caches silently local to each process:

    CacheFlow.configure({
    redis: {
    host: 'your.redis.server.com'
    // no port: throws Error: Invalid Cache Flow Redis configuration: both 'host' and 'port' are required
    }
    });

    Omitting the redis block entirely is still perfectly valid, and keeps all caches in in-memory LRU mode.

    But what if your Redis server has to restart or goes down? Don't worry, Cache Flow has got you covered!

    In case your Redis server temporarily goes down, all your caches will automatically fallback to an in-memory LRU cache, until your Redis server is back online. As soon as your caches can reconnect, they'll switch back to using Redis. This way, your caching layer keeps serving requests throughout.

    Note that each switch between Redis and LRU starts from an empty cache, so the first get for any given key after a switch calls your loader again. Availability is preserved; cached entries are not carried across.

    Cache Flow comes with a default logger which can log various information about what happens with your caches (when values are get, loaded, some errors, ...)

    By default, Cache Flow will only log errors to console.error, but you can provide your own logger in the initial configuration of the library, like log4js , winston, ...

    See the example below with log4js:

    import { CacheFlow } from 'cache-flow';
    import * as log4js from 'log4js';

    CacheFlow.configure({
    logger: log4js.getLogger()
    });

    You can also simply pass the console to get all logs output to the stdout, including debug logs, like so:

    import { CacheFlow } from 'cache-flow';

    CacheFlow.configure({
    logger: console
    });
    1. CacheLoader constructor parameters:
    • cacheId: a unique string identifying each cache. If shared between 2 caches or more, their keys might conflict, and cause deserialization errors when trying to get a key stored by another cache.
    • options:
      • expirationTime: the time in seconds during which cache entries will be retained before being evicted (default: 86400, i.e. 24 hours). Note that @Cacheable defaults to 3600 (1 hour) instead.
      • maxSize: the maximum number of entries stored in the cache when running in LRU mode (default: 1000). Once maximum is reached and a new entry is added to the cache, it replaces the least recently used. This option has no effect in Redis mode, where entry count is bounded by your Redis server configuration.
    1. CacheFlow.configure(configuration) configuration object parameter:
    • redis: optional. When given, both host and port are required, and configure throws otherwise.
      • host: the Redis server hostname
      • port: the Redis server port
      • db: the Redis database index to use (default: 0)
    • logger: a logger instance matching LoggerInterface. When configure is called without a logger, the logger is reset to the default one, which only writes errors to console.error.

    When extending CacheLoader, your cache will come with default serialization and deserialization implementations for the serialize and deserialize methods, which take care of storing objects and arrays as JSON in the inner cache, and transform them back when getting values out of the cache.

    In the case where you need to implement your own serialization and deserialization, for example to cache specific framework classes or entities, your CacheLoader implementation can override serialize and deserialize methods:

    class MyEntityCache extends CacheLoader<string, MyEntity> {

    constructor() {
    super('my-entity-cache', {
    expirationTime: 3600 // 1h in seconds
    });
    }

    protected async load(id: string): Promise<MyEntity> {
    const entity = await repository.findById(id);
    return entity;
    }

    protected serialize(entity: MyEntity): any {
    return entity.toJSON();
    }

    protected deserialize(serialized: any): MyEntity {
    return MyEntity.fromJSON(serialized);
    }

    }

    Cache Flow treats null and undefined as "nothing cached for this key", and every other value — including falsy ones — as a cacheable value:

    Value returned by your loader Served from the cache on the next get
    0, '', false yes
    any string, object or array yes
    null no, your loader is called on every get
    undefined no, your loader is called on every get

    So a loader that legitimately resolves to 0, '' or false is cached and served like any other value:

    class StockCache extends CacheLoader<string, number> {

    constructor() {
    super('stock-cache', {
    expirationTime: 60
    });
    }

    protected async load(sku: string): Promise<number> {
    return warehouse.countAvailable(sku); // may legitimately be 0
    }

    }

    const cache = new StockCache();
    await cache.get('sku-123'); // 0, counted by the warehouse
    await cache.get('sku-123'); // 0, served from the cache — your loader is not called again

    If your loader returns null or undefined to mean "there is no value for this key", nothing is stored and nothing is ever served from the cache for it, so your loader runs on every call. When those lookups are expensive, return a sentinel value your own code recognises instead of null, so the negative result gets cached too.

    Two related behaviours worth knowing:

    • get(undefined) returns undefined straight away, without calling your loader.
    • set(key, undefined) and set(undefined, value) store nothing and log an error.

    Objects and arrays are stored as JSON in both LRU and Redis mode, so Date values come back as ISO strings, and Map, Set and class instances lose their type. Override serialize and deserialize (see above) when you need them restored.

    NaN and Infinity are the one case where the two modes differ: LRU mode caches and returns them unchanged, while Redis mode stores them as null and therefore treats them as absent, calling your loader on every get.

    Cache Flow is compatible with your favorite Typescript DI framework, like typedi, tsyringe, InversifyJS...

    For example, with typedi:

    import { CacheLoader } from 'cache-flow';
    import { Inject, Service } from 'typedi';

    @Service()
    class UserCache extends CacheLoader<string, User> {

    @Inject()
    private userService: UserService;

    constructor() {
    super('user-cache', {
    expirationTime: 3600 // 1h in seconds
    });
    }

    protected async load(userId: string): Promise<User> {
    return this.userService.getUserById(userId);
    }

    }

    Also see the code example

    Full reference

    Alongside CacheLoader, CacheFlow and Cacheable, the package entry point exports the types you need to annotate your own code: CacheOptions, CacheDefinition, CacheMetadata, Metadata, CacheFlowConfiguration, RedisCacheConfiguration, LoggerInterface and DefaultLogger.

    import { CacheFlowConfiguration, LoggerInterface, Metadata } from 'cache-flow';
    

    Full reference

    Method Example Description
    async get(key: K, force?: boolean): Promise<V> myCache.get('myKey') Gets a value from the cache, loading it if no value is cached. If force is set to true, a new value is loaded without checking existence in the cache.
    async getWithMetadata(key: K, force?: boolean): Promise<Metadata<V>> myCache.getWithMetadata('myKey') Same as get, returning { value, cached, time }: whether the value came from the cache, and how many milliseconds the call took.
    async set(key: K, value: V): Promise<void> myCache.set('myKey', 'myValue') Sets a value in the cache for the given key
    async delete(key: K): Promise<void> myCache.delete('myKey') Evicts a key from the cache
    async exists(key: K): Promise<boolean> myCache.exists('myKey') Checks whether a value exists in the cache for the given key
    async reset(): Promise<void> myCache.reset() Clears all values from the cache
    getCacheId(): string myCache.getCacheId() Gets the cache's ID
    getCacheDefinition(): CacheDefinition myCache.getCacheDefinition() Gets the cache definition

    These are the methods you call. The ones you can override in your own CacheLoader are load (required), and keyToString, serialize and deserialize (all optional).

    Full reference

    Method Example Description
    static configure(configuration: CacheFlowConfiguration) CacheFlow.configure({redis: {host: 'localhost', port: 6379}}) Sets the global configuration for Cache Flow and all subsequently instantiated caches. Call it before instantiating any cache. Throws on an incomplete redis configuration.
    static get(cacheId: string): CacheLoader<any, any> CacheFlow.get('my-cache') Gets cache with given cache ID. Cannot access caches created using @Cacheable annotation.
    static async delete(cacheId: string, ...key: any[]): Promise<void> CacheFlow.delete('my-cache', 'myKey') Deletes entry for given key in cache with given cache ID. For an @Cacheable cache, pass the method arguments in order: CacheFlow.delete('MyClass#myMethod', 'foo', 123).
    static async reset(cacheId: string): Promise<void> CacheFlow.reset('my-cache') Resets cache with given cache ID, including @Cacheable caches
    static async resetAll(): Promise<void> CacheFlow.resetAll() Clears all caches, including @Cacheable caches
    static getInstances(): CacheLoader<any, any>[] CacheFlow.getInstances() Gets all cache instances (except the ones created using @Cacheable annotation)

    Unlike get and getInstances, delete and reset do reach @Cacheable caches, addressed by their cache ID — which defaults to ClassName#methodName when you do not set one.

    Project Information

    👤 Alexandre Bourdin contact@abourdin.com

    Give a ⭐️ if this project helped you!