"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const settings_1 = require("./settings");
let Redis = require("ioredis");
class RedisStore {
    constructor(tag = "tag", autoclose = false) {
        this.interval = null;
        this.redis = null;
        this.waitForStatus = false;
        this.autoclose = false;
        this.NO_REDIS_CONNECTION = "A Redis connection cannot be established";
        this.config = settings_1.default.Instance.getRedis(tag);
        let statusInterval = this.config.statusCheckInterval;
        if (statusInterval == null || typeof statusInterval != "number" || statusInterval < 1) {
            statusInterval = 5000;
        }
        this.status = "disconnected";
        this.autoclose = autoclose;
        this.tag = tag;
        this.redisStatusCheck();
        this.interval = setInterval(this.redisStatusCheck.bind(this), statusInterval);
    }
    destroy() {
        if (this.interval != null)
            clearInterval(this.interval);
        if (this.redis == null)
            return;
        this.redis.quit();
        this.redis = null;
    }
    connect() {
        if (this.config.isCluster) {
            this.redis = new Redis({
                sentinels: this.config.sentinels,
                name: this.config.sentinel_cluster_name,
                password: this.config.password,
                sentinelRetryStrategy: function (times) {
                    // allow 3 retries only
                    if (times > 3) {
                        // block reconnecting by returning a non-number value
                        return null;
                    }
                    var delay = Math.min(times * 50, 2000);
                    return delay;
                }
            });
        }
        else {
            this.redis = new Redis({
                port: this.config.port,
                host: this.config.host,
                family: this.config.family,
                password: this.config.password,
                retryStrategy: function (times) {
                    if (times < 3) {
                        return Math.min(times * 100, 2000);
                    }
                    return "";
                }
            });
        }
        //add custom spop_keyset command
        if (this.redis != null)
            this.redis.defineCommand("spop_keyset", {
                numberOfKeys: 3,
                lua: "redis.replicate_commands(); local session=redis.call('SPOP',KEYS[1]); redis.call('SET', KEYS[2]..':'..session, KEYS[3])return session"
            });
    }
    redisStatusCheck() {
        if (this.waitForStatus)
            return;
        this.waitForStatus = true;
        let self = this;
        let rediskey_connections = "redis_connection_test";
        if (this.config.keys != undefined && this.config.keys.rediskey_connections != "") {
            rediskey_connections = this.config.keys.rediskey_connections;
        }
        this.set(rediskey_connections + this.tag, "ok", function (isSucces, result) {
            if (isSucces) {
                self.status = "connected";
            }
            else {
                self.status = "disconnected";
            }
            self.waitForStatus = false;
        });
    }
    ensureIsConnected() {
        if (this.redis == null || this.redis.status == null || this.redis.status == "end") {
            if (this.config.autoConnect) {
                this.connect();
                return true;
            }
        }
        return false;
    }
    /**
     * return the status of the inner redis connection
     */
    getStatus() {
        return this.status;
    }
    isFunctional() {
        return this.getStatus() == "connected";
    }
    /**
     * close the inner redis connection
     */
    close() {
        if (this.redis)
            this.redis.quit();
    }
    /**
     * update the value of a key in redis
     * THIS IS ONLY HERE TO PROVIDE BACKWARDS COMPATIBILITY
     */
    set(key, value, callback) {
        this.set_with_ttl(key, value, undefined, callback);
    }
    /**
     * update the value of a key in redis
     */
    set_with_ttl(key, value, ttl, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        //callback always last
        let _callback = function (err, result) {
            if (typeof callback == "function") {
                if (err != null && err != undefined) {
                    callback(false, err);
                }
                else {
                    callback(true, result);
                }
            }
            if (self.autoclose)
                self.close();
        };
        //maybe we have ttl
        if ((ttl != undefined) && (typeof ttl == 'number')) {
            this.redis.set(key, value, 'EX', ttl, _callback);
        }
        else {
            this.redis.set(key, value, _callback);
        }
    }
    hset(key, field, value, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.hset(key, field, value, function (err, result) {
            if (typeof callback == "function") {
                if (err != null && err != undefined) {
                    callback(false, err);
                }
                else {
                    callback(true, result);
                }
            }
            if (self.autoclose)
                self.close();
        });
    }
    /**
     * get the keys from redis
     */
    keys(key, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.keys(key, function (err, result) {
            if (err != null && err != undefined) {
                callback(false, err);
            }
            else {
                callback(true, result);
            }
            if (self.autoclose)
                self.close();
        });
    }
    /**
     * get the value of a key stored in redis
     */
    get(key, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.get(key, function (err, result) {
            if (err != null && err != undefined) {
                callback(false, err);
            }
            else {
                callback(true, result);
            }
            if (self.autoclose)
                self.close();
        });
    }
    hget(key, field, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.hget(key, field, function (err, result) {
            if (err != null && err != undefined) {
                callback(false, err);
            }
            else {
                callback(true, result);
            }
            if (self.autoclose)
                self.close();
        });
    }
    hgetall(key, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.hgetall(key, function (err, result) {
            if (err != null && err != undefined) {
                callback(false, err);
            }
            else {
                callback(true, result);
            }
            if (self.autoclose)
                self.close();
        });
    }
    /**
     * Callback is optional, it is not necesarylly to send it
     * @param key
     * @param callback
     */
    delete(key, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.del(key, function (err, result) {
            if (typeof callback == "function") {
                if (err != null && err != undefined) {
                    callback(false, err);
                }
                else {
                    callback(true, result);
                }
            }
            if (self.autoclose)
                self.close();
        });
    }
    hdel(key, field, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.hdel(key, field, function (err, result) {
            if (typeof callback == "function") {
                if (err != null && err != undefined) {
                    callback(false, err);
                }
                else {
                    callback(true, result);
                }
            }
            if (self.autoclose)
                self.close();
        });
    }
    begin_transaction() {
        this.ensureIsConnected();
        if (!this.redis)
            return null;
        return this.redis.multi();
    }
    exec_transaction(redis_transactions, callback) {
        this.ensureIsConnected();
        let self = this;
        redis_transactions.exec(function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    countValues(key, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.keys(key + ":*", function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    /**
     * get the number of keys found under the used session key
     */
    count_used_sessions(used_sessions_key, callback) {
        this.countValues(used_sessions_key, function (result, data) {
            callback(result, data);
        });
    }
    add_to_set(setName, values, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.sadd(setName, values, function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        }.bind(this));
    }
    /**
     * sessions can be an array or a single value. eg: [1, 2, 3] or 1, 2, 3 or 2 or [2]
     */
    add_session(sessions, free_sessions_set_key, callback) {
        let self = this;
        this.add_to_set(free_sessions_set_key, sessions, function (result, data) {
            callback(result, data);
            if (self.autoclose)
                self.close();
        });
    }
    remove_from_set(setName, member, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.srem(setName, member, function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    watch_key(key) {
        this.ensureIsConnected();
        if (!this.redis)
            return;
        this.redis.watch(key);
    }
    unwatch_keys() {
        this.ensureIsConnected();
        if (!this.redis)
            return;
        this.redis.unwatch();
    }
    pop_from_set(setName, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.spop(setName, function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    spop(setName, count = 1, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.spop(setName, count, function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    smembers(key, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.smembers(key, function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    /**
     * get the number of elements in the given set
     */
    setCount(key, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.scard(key, function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    add_to_list(listName, values, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.lpush(listName, values, function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    remove_from_list(listName, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.rpop(listName, function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    /**
     * moves the tail element from list 'sourceList' into the list 'destinationList'
     * @param sourceList
     * @param destinationList
     * @param callback
     */
    move(sourceList, destinationList, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.rpoplpush(sourceList, destinationList, function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    countList(listName, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.llen(listName, function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    rPoplpush(sourceList, destinationList, callback) {
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        let self = this;
        this.redis.rpoplpush(sourceList, destinationList, function (err, results) {
            if (err != undefined && err != null) {
                callback(false, err);
            }
            else {
                callback(true, results);
            }
            if (self.autoclose)
                self.close();
        });
    }
    /**
     * get a session randomly from the free session set and move it to the used sessions
     *
     * Implemented Optimistic locking using check-and-set : https://redis.io/topics/transactions
     * This implementation in theory is safer but could introduce new situations, use it with caution as it is still experimental
     */
    lock_session_with_watch(free_sessions_set_key, used_sessions_key, lock_duration = 10 * 60 * 1000, callback) {
        let self = this;
        let timestamp = new Date().getTime() + lock_duration; //add 10 minutes to current time of the server
        //lock key//DONT forget to unlock
        this.watch_key(free_sessions_set_key);
        //pop a session from the set
        this.pop_from_set(free_sessions_set_key, function (status, session) {
            if (status) {
                let transaction = self.begin_transaction();
                //failed to create a transaction
                if (!transaction) {
                    self.unwatch_keys();
                    callback(false, new Error("Unable to start transaction"));
                    return;
                }
                //check if we got a session
                if (session) {
                    //set up the transaction
                    transaction
                        .set(used_sessions_key + ":" + session, timestamp);
                    //execute it
                    self.exec_transaction(transaction, function (status, result) {
                        //return session if status is true else return the result which should contain the error
                        callback(status, status ? session : result);
                    });
                }
                else {
                    self.unwatch_keys();
                    callback(false, new Error('no free session available'));
                }
            }
            else {
                self.unwatch_keys();
                callback(status, session);
            }
        });
    }
    /**
     * get a session randomly from the free session set and move it to the used sessions
     * @deprecated use lock_session_lua instead
     */
    lock_session(free_sessions_set_key, used_sessions_key, lock_duration = 10 * 60 * 1000, callback) {
        let self = this;
        let timestamp = new Date().getTime() + lock_duration; //add 10 minutes to current time of the server
        this.pop_from_set(free_sessions_set_key, function (status, session) {
            if (status) {
                if (session)
                    self.set(used_sessions_key + ":" + session, timestamp, function (status, result) {
                        //return session if status is true else return the result which should contain the error
                        callback(status, status ? session : result);
                    });
                else
                    callback(false, new Error('no free session available'));
            }
            else {
                callback(status, session);
            }
        });
    }
    /**
    * get a session randomly from the free session set and move it to the used sessions
    */
    lock_session_lua(free_sessions_set_key, used_sessions_key, lock_duration = 10 * 60 * 1000, callback) {
        let self = this;
        let timestamp = new Date().getTime() + lock_duration; //add 10 minutes to current time of the server
        this.ensureIsConnected();
        if (!this.redis) {
            if (typeof callback == "function")
                callback(false, new Error(this.NO_REDIS_CONNECTION));
            return;
        }
        if (self.redis && self.redis.spop_keyset) {
            self.redis.spop_keyset(free_sessions_set_key, used_sessions_key, timestamp).then(function (result) {
                callback(true, result);
            }).catch(function (err) {
                callback(false, new Error('error selecting a session'));
            });
        }
        else {
            callback(false, new Error('error accessing redis'));
        }
    }
    /**
     * move a session from the used section the free or error sets
     */
    unlock_session(session, isErr, free_sessions_set_key, used_sessions_key, error_sessions_set_key, callback) {
        let transaction = this.begin_transaction();
        if (!transaction) {
            callback(false, new Error("Unable to start transaction"));
            return;
        }
        transaction
            .del(used_sessions_key + ":" + session)
            .sadd((isErr ? error_sessions_set_key : free_sessions_set_key), session);
        this.exec_transaction(transaction, function (result, data) {
            //return session if status is true else return the result which should contain the error
            callback(result, result ? session : data);
        });
    }
    rpoplpush_with_counter(destination_list, value, counter, callback) {
        let self = this;
        this.countList(destination_list, function (result, data) {
            if (result) {
                if (data < counter)
                    self.add_to_list(destination_list, value, function (result, data) {
                        callback(result, data);
                    });
                else {
                    // now perform the transaction
                    let transaction = self.begin_transaction();
                    if (!transaction) {
                        callback(false, new Error("Unable to start transaction"));
                        return;
                    }
                    transaction
                        .lpush(destination_list, value)
                        .rpop(destination_list);
                    self.exec_transaction(transaction, function (result, data) {
                        if (result) {
                            callback(true, data);
                        }
                        else {
                            callback(false, data);
                        }
                    });
                }
            }
            else
                callback(false, data);
        });
    }
    ;
    /**
     * wrap a method in promisefy
     *
     * @param method
     * @param args
     */
    promisefy(method, ...args) {
        return __awaiter(this, void 0, void 0, function* () {
            let self = this;
            //i hope this is not called with both callbacks and promisefy
            // if(args.length > 0){
            //     let lastArg = args[args.length - 1];
            //     if(typeof lastArg == "function"){
            //        args.splice(-1,1)
            //     }
            // }
            return new Promise(function (resolve, reject) {
                method.bind(self)(...args, function (succes, data) {
                    if (succes)
                        resolve(data);
                    else
                        reject(data);
                });
            });
        });
    }
}
exports.default = RedisStore;
//# sourceMappingURL=redis.js.map