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.
- Installation
- Usage Example
- Configuration
- Custom serialization/deserialization
- Usage with dependency injection
- Cache Flow Reference
- Install the library
npm install --save cache-flow
-
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';
- 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
- Use your cache
const cache = new SimpleCache();
const myValue = cache.get('myKey');
setTimeout(function () {
const myValue2 = cache.get('myKey');
console.log(myValue);
console.log(myValue2); // myValue2 has the same value as myValue!
}, 3000);
- 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) => {
user.id, user.lastUpdate
},
options: {
expirationTime: 3600
}
})
If argsToKey
returns a string, this string is directly used a the cache key. Otherwise, the cache computes a hash of
the returned value and uses it as the cache key.
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();
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, you will never experience any interruption in your caching layer.
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
});
-
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 -
maxSize
: the maximum number of entries stored in the cache when running in LRU mode. Once maximum is reached and a new entry is added to the cache, it replaces the least recently used.
-
-
CacheFlow.configure(configuration)
configuration object parameter:
-
redis
:-
host
: the Redis server hostname -
port
: the Redis server port (default: 6379) -
db
: the Redis database index to use (default: 0)
-
-
logger
: a logger instance matchingLoggerInterface
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 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
Method | Example | Description |
---|---|---|
async get(key: K, force: boolean): Promise<V> |
myCache.get('myKey') |
Gets a value from the cache. 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.get('myKey') |
Gets a value from the cache with additional metadata (loading time, caching status, ...). If force is set to true, a new value is loaded without checking existence in the cache. |
async set(key: K, value: V): Promise<void> |
myCache.set('myKey', 'myValue') |
Sets a value from 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 |
Method | Example | Description |
---|---|---|
static configure(configuration: CacheFlowConfiguration) |
CacheFlow.configure({redis: {port: 1234}}) |
Sets the global configuration for Cache Flow and all subsequently instantiated caches |
static get(cacheId: string): Promise<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 |
static async reset(cacheId: string): Promise<void> |
CacheFlow.reset('my-cache') |
Resets cache with given cache ID |
static async resetAll(): Promise<void> |
CacheFlow.resetAll() |
Clears all caches |
static getInstances(): CacheLoader<any, any>[] |
CacheFlow.getInstances() |
Gets all cache instances (except the ones created using @Cacheable annotation) |
👤 Alexandre Bourdin contact@abourdin.com
- Github: @abourdin
- Website: https://www.abourdin.com
- LinkedIn: @alexandre-bourdin-38090844
Give a ⭐️ if this project helped you!