Ghost/ghost/adapter-cache-redis/lib/AdapterCacheRedis.js
Fabien "egg" O'Carroll 104f84f252 Added eslint rule for file naming convention
As discussed with the product team we want to enforce kebab-case file names for
all files, with the exception of files which export a single class, in which
case they should be PascalCase and reflect the class which they export.

This will help find classes faster, and should push better naming for them too.

Some files and packages have been excluded from this linting, specifically when
a library or framework depends on the naming of a file for the functionality
e.g. Ember, knex-migrator, adapter-manager
2023-05-09 12:34:34 -04:00

159 lines
5.1 KiB
JavaScript

const BaseCacheAdapter = require('@tryghost/adapter-base-cache');
const logging = require('@tryghost/logging');
const cacheManager = require('cache-manager');
const redisStore = require('cache-manager-ioredis');
const calculateSlot = require('cluster-key-slot');
class AdapterCacheRedis extends BaseCacheAdapter {
/**
*
* @param {Object} config
* @param {Object} [config.cache] - caching instance compatible with cache-manager with redis store
* @param {String} [config.host] - redis host used in case no cache instance provided
* @param {Number} [config.port] - redis port used in case no cache instance provided
* @param {String} [config.password] - redis password used in case no cache instance provided
* @param {Object} [config.clusterConfig] - redis cluster config used in case no cache instance provided
* @param {Number} [config.ttl] - default cached value Time To Live (expiration) in *seconds*
* @param {String} [config.keyPrefix] - prefix to use when building a unique cache key, e.g.: 'some_id:image-sizes:'
*/
constructor(config) {
super();
this.cache = config.cache;
if (!this.cache) {
// @NOTE: this condition can be avoided if we add merging of nested options
// to adapter configuration. Than adding adapter-specific {clusterConfig: {options: {ttl: XXX}}}
// will be enough to set ttl for redis cluster.
if (config.ttl && config.clusterConfig) {
if (!config.clusterConfig.options) {
config.clusterConfig.options = {};
}
config.clusterConfig.options.ttl = config.ttl;
}
this.cache = cacheManager.caching({
store: redisStore,
ttl: config.ttl,
host: config.host,
port: config.port,
password: config.password,
clusterConfig: config.clusterConfig
});
}
this.keyPrefix = config.keyPrefix;
this._keysPattern = config.keyPrefix ? `${config.keyPrefix}*` : '';
this.redisClient = this.cache.store.getClient();
this.redisClient.on('error', this.handleRedisError);
}
handleRedisError(error) {
logging.error(error);
}
#getPrimaryRedisNode() {
const slot = calculateSlot(this.keyPrefix);
const [ip, port] = this.redisClient.slots[slot][0].split(':');
for (const node of this.redisClient.nodes()) {
if (node.options.host === ip && node.options.port === parseInt(port)) {
return node;
}
}
return null;
}
#scanNodeForKeys(node) {
return new Promise((resolve, reject) => {
const stream = node.scanStream({match: this._keysPattern, count: 100});
let keys = [];
stream.on('data', (resultKeys) => {
keys = keys.concat(resultKeys);
});
stream.on('error', (e) => {
reject(e);
});
stream.on('end', () => {
resolve(keys);
});
});
}
/**
* This is a recommended way to build cache key prefixes from
* the cache-manager package. Might be a good contribution to make
* in the package itself (https://github.com/node-cache-manager/node-cache-manager/issues/158)
* @param {string} key
* @returns {string}
*/
_buildKey(key) {
if (this.keyPrefix) {
return `${this.keyPrefix}${key}`;
}
return key;
}
/**
* This is a method to remove the key prefix from any raw key returned from redis.
* @param {string} key
* @returns {string}
*/
_removeKeyPrefix(key) {
return key.slice(this.keyPrefix.length);
}
/**
*
* @param {String} key
*/
async get(key) {
try {
return await this.cache.get(this._buildKey(key));
} catch (err) {
logging.error(err);
}
}
/**
*
* @param {String} key
* @param {*} value
*/
async set(key, value) {
try {
return await this.cache.set(this._buildKey(key), value);
} catch (err) {
logging.error(err);
}
}
async reset() {
// NOTE: dangerous in shared environment, and not used in production code anyway!
// return await this.cache.reset();
logging.error('Cache reset has not been implemented with shared cache environment in mind');
}
/**
* Helper method to assist "getAll" type of operations
* @returns {Promise<Array<String>>} all keys present in the cache
*/
async keys() {
try {
const primaryNode = this.#getPrimaryRedisNode();
if (primaryNode === null) {
return [];
}
const rawKeys = await this.#scanNodeForKeys(primaryNode);
return rawKeys.map((key) => {
return this._removeKeyPrefix(key);
});
} catch (err) {
logging.error(err);
}
}
}
module.exports = AdapterCacheRedis;