@redpill-paris/quidol-redis-cache
Version:
Quidol redis cache sdk
186 lines (166 loc) • 5.29 kB
JavaScript
const { Mutex } = require('async-mutex');
const Redis = require('ioredis');
const objectHash = require('object-hash');
const redisMap = {};
const defaults = { scaleReads: "slave", enableOfflineQueue: false };
/*
** type: can be standalone, cluster or sentinel
*/
class Cache {
constructor({
defaultTTL = 60,
type = 'standalone',
redisClusterOptions,
redisSentinelOptions,
redisOptions,
options = defaults
}) {
if(!options) options = { scaleReads: "slave" };
switch(type) {
case 'standalone':
if(!redisOptions) throw new Error(`No redisOptions specified for type:${type}`);
this.setupStandalone({...redisOptions, ...options});
break;
case 'cluster':
if(!redisClusterOptions) throw new Error(`No redisClusterOptions specified for type:${type}`);
this.setupCluster(redisClusterOptions, options);
break;
case 'sentinel':
if(!redisSentinelOptions) throw new Error(`No redisSentinelOptions specified for type:${type}`);
this.setupSentinel({...redisSentinelOptions, ...options});
break;
default:
throw new Error(`Unknown type:${type}`);
}
this.type = type;
this.defaultTTL = defaultTTL;
this.cache = this.redis;
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.mutexList = {};
}
setupSentinel(clusterOptions) {
const optionsHash = objectHash(clusterOptions);
if (!redisMap[optionsHash]) {
redisMap[optionsHash] = new Redis(clusterOptions);
}
this.redis = redisMap[optionsHash];
}
setupCluster(clusterOptions, options) {
const optionsHash = objectHash(clusterOptions);
if (!redisMap[optionsHash]) {
redisMap[optionsHash] = new Redis.Cluster(clusterOptions, options);
}
this.redis = redisMap[optionsHash];
}
setupStandalone(redisOptions) {
const { host, port } = redisOptions;
if (!host || !port) {
throw new Error('No port or host specified for redisOptions');
}
if (!redisMap[`${host}:${port}`]) {
redisMap[`${host}:${port}`] = new Redis(redisOptions);
}
this.redis = redisMap[`${host}:${port}`];
this.redisOptions = redisOptions;
}
async get(key, storeFunction, ttl = this.defaultTTL) {
if (!this.mutexList[key]) this.mutexList[key] = new Mutex();
const mutex = this.mutexList[key];
const value = await this.cache.get(key);
if (value) {
return Promise.resolve(JSON.parse(value));
}
const release = await mutex.acquire();
try {
const newValueLocked = await this.cache.get(key);
if (!newValueLocked) {
if (storeFunction !== undefined) {
const result = await storeFunction();
await this.cache.set(key, JSON.stringify(result), 'EX', (!ttl) ? 0 : ttl);
return result;
}
} else {
return JSON.parse(newValueLocked);
}
return undefined;
} finally {
release();
delete this.mutexList[key];
}
}
set(key, value, ttl = this.defaultTTL) {
return this.cache.set(key, JSON.stringify(value), 'EX', (!ttl) ? 0 : ttl);
}
del(...keys) {
if (this.type === 'cluster') {
return Promise.all(keys.map(key => this.cache.del(key)));
}
if (keys.length > 0) return this.cache.del(...keys);
return undefined;
}
async delStartWith(startStr = '') {
if (!startStr) {
return;
}
if (this.type === 'cluster') {
const masters = this.cache.nodes('master');
const keysAllNodes = await Promise.all(masters.map((node) => node.keys(`${startStr}*`)));
return Promise.all(keysAllNodes.map((keys) => {
return Promise.all(keys.map((key) => {
return this.cache.del(key);
}))
}));
} else {
const keys = await this.cache.keys(`${startStr}*`);
return Promise.all(keys.map((key) => {
this.cache.del(key);
}));
}
}
delAll(match, count = 100) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Timeout has occurred in delAll'))
}, 10000);
const promises = [];
const stream = this.cache.scanStream({ count, match });
let pipeline = this.cache.pipeline();
let nbWaiting = 0;
stream.on('data', (keys) => {
keys.forEach((key) => {
pipeline.del(key);
});
nbWaiting += keys.length;
if (nbWaiting > count) {
promises.push(new Promise(resolve => pipeline.exec(resolve)));
nbWaiting = 0;
pipeline = this.cache.pipeline();
}
});
stream.on('end', async () => {
promises.push(new Promise(resolve => pipeline.exec(resolve)));
await Promise.all(promises);
clearTimeout(timeout);
resolve();
});
stream.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
});
}
flush() {
this.cache.flushall('ASYNC');
}
static resetConnectionMap() {
Object.keys(redisMap).map((key) => delete redisMap[key]);
}
static getDefaults() {
return defaults;
}
zrevrank(key, start, end, withscore) {
return this.cache.zrevrange(key, start, end, withscore);
}
}
module.exports = Cache;