scubadiver214
5/2/2017 - 1:01 AM

redis wrapper

redis wrapper

'use strict';

/**
 * Wrapper for general/all purpose shared caching (clustered)
 * @module ./RedisCache
 *
 * Options:
 * {
 *     connectUrl: 'redis://127.0.0.1:6379', // (required) url connection to redis
 *     enabled: true, // (default: true) indicator if redis should be used or bypassed
 *     expiration: 1209600, // (required) default ttl in seconds for each value. can be overridden for each pair
 *     errorHandler: function(err) (optional) error handler
 * }
 */

const _ = require('lodash');
const redis = require('redis');
const Q = require('q');
const errorHelper = require('./error-helper');

let RedisCache = function (options) {
    this.options = options ? _.cloneDeep(options) : {};
    this.options.enabled = _.get(this.options, "enabled", true);
    this.client = null;
};

function handleResult(result) {
    if(!this.options.errorHandler) {
        return result;
    }
    if(!result || !result.catch) {
        return this.options.errorHandler(result);
    }
    return result.catch(err => {
        return this.options.errorHandler(err);
    });
}

function exec(func) {
    return _.flow(func, handleResult);
}

/**
 * Set in cache a key/value combination
 * requires you provide key, value and expiration.
 * expiration can either be in date or seconds
 * does not return a result if all is successful
 */
RedisCache.prototype.set = exec(function(key, value, options) {
    value = JSON.stringify(value);
    return this.setRaw(key, value, options);
});

/**
 * Set a value in the redis cache without stringifying the value
 */
RedisCache.prototype.setRaw = exec(function(key, value, options) {

    options = options || {};

    if(!this.options.enabled) {
        return Q();
    }

    if(!key || !value){
        return Q.reject(new Error('Invalid key value set'));
    }

    var timeToLive  = options.expiration || this.options.expiration;

    if(!timeToLive){
        return Q.reject(new Error('Expiration not provided for key: ' + key));
    }

    var isExpirationDate = Object.prototype.toString.call(timeToLive) === '[object Date]';

    if(isExpirationDate){
        var time = timeToLive.getTime() - new Date().getTime();
        timeToLive = Math.ceil(Math.abs(time / 1000));
    }

    return this.getClient()
        .then(function(client) {
            return Q.ninvoke(client, 'setex', key, timeToLive, value);
        })
        .then(function(result) {
            if(!result || result.toLowerCase() !== 'ok') {
                return Q.reject(new Error('Failed to cache: ' + key));
            }
        });
});

/**
 * Get from cache (key)
 * requires you provide key you want the value for
 * parses a string value with JSON.parse
 */
RedisCache.prototype.get = exec(function(key) {
    return this.getRaw(key)
        .then(value => {
            if(_.isString(value)) {
                value = JSON.parse(value);
            }
            return value;
        });
});

/**
 * Get from cache (key)
 * requires you provide key you want the value for
 */
RedisCache.prototype.getRaw = exec(function(key) {

    if (!this.options.enabled) {
        return Q();
    }

    if (!key) {
        return Q.reject(new Error('Invalid key'));
    }

    return this.getClient()
        .then(client => {
            return Q.ninvoke(client, 'get', key);
        });
});

/**
 * Delete from cache (key)
 * requires you provide key you want to delete
 */
RedisCache.prototype.delete = exec(function(key) {

    if(!this.options.enabled) {
        return;
    }

    if(!key){
        return Q.reject(new Error('Invalid key'));
    }

    return this.getClient()
        .then(function(client){
            return Q.ninvoke(client, 'del', key);
        });
});

/**
 * Checks if the (key) exists in cache
 * requires you provide key you want to check for
 */
RedisCache.prototype.exists = exec(function(key) {

    if(!this.options.enabled) {
        return false;
    }

    if(!key){
        return Q.reject(new Error('Invalid key'));
    }

    return this.getClient()
        .then(function(client){
            return Q.ninvoke(client, 'exists', key);
        });
});

/**
 * Returns the redis client
 * creates the client and connects to it
 */
RedisCache.prototype.getClient = function() {

    if(!this.options.enabled) {
        return Q.reject(new Error('disabled'));
    }

    if(this.client) {
        return Q(this.client);
    }

    if(!this.options.connectUrl) {
        return Q.reject(new Error('missing connectUrl'));
    }

    var client = redis.createClient(this.options.connectUrl);

    return Q.Promise(function(resolve, reject){
        client.on('connect', function(){
            this.client = client;
            resolve(this.client);
        }.bind(this));
        client.on('error', function(err){
            err = errorHelper.ensureError(err, 'Unknown getClient redis error');
            this.client = null;
            handleResult.call(this, err);
            reject(err);
        }.bind(this));
        client.on('end', function(){
            this.client = null;
            resolve(null);
        }.bind(this));
    }.bind(this));
};

module.exports = RedisCache;