"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const events_1 = require("events");
const types_1 = require("@deepstream/types");
const constants_1 = require("../../constants");
/**
 * The lock registry is responsible for maintaing a single source of truth
 * within the cluster, used mainly for issuing cluster wide locks when an operation
 * that stretches over multiple nodes are required.
 *
 * For example, distributed listening requires a leader to drive the nodes in sequence,
 * so issuing a lock prevents multiple nodes from assuming the lead.
 *
 */
class DistributedLockRegistry extends types_1.DeepstreamPlugin {
    /**
     * The unique registry is a singleton and is only created once
     * within deepstream.io. It is passed via
     * via the options object.
     */
    constructor(pluginOptions, services, config) {
        super();
        this.pluginOptions = pluginOptions;
        this.services = services;
        this.config = config;
        this.description = 'Distributed Lock Registry';
        this.locks = new Set();
        this.timeouts = new Map();
        this.responseEventEmitter = new events_1.EventEmitter();
        this.onPrivateMessage = this.onPrivateMessage.bind(this);
    }
    init() {
        this.services.clusterNode.subscribe(constants_1.TOPIC.LOCK, this.onPrivateMessage);
    }
    /**
     * Requests a lock, if the leader ( whether local or distributed ) has the lock availble
     * it will invoke the callback with true, otherwise false.
     */
    get(lockName, callback) {
        if (this.services.clusterRegistry.isLeader()) {
            callback(this.getLock(lockName));
        }
        else if (!this.timeouts.has(lockName)) {
            this.getRemoteLock(lockName, callback);
        }
        else {
            callback(false);
        }
    }
    /**
     * Release a lock, allowing other resources to request it again
     */
    release(lockName) {
        if (this.services.clusterRegistry.isLeader()) {
            this.releaseLock(lockName);
        }
        else {
            this.releaseRemoteLock(lockName);
        }
    }
    /**
     * Called when the current node is not the leader, issuing a lock request
     * via the message bus
     */
    getRemoteLock(lockName, callback) {
        const leaderServerName = this.services.clusterRegistry.getLeader();
        this.timeouts.set(lockName, setTimeout(this.onLockRequestTimeout.bind(this, lockName), this.pluginOptions.requestTimeout));
        this.responseEventEmitter.once(lockName, callback);
        this.services.clusterNode.sendDirect(leaderServerName, {
            topic: constants_1.TOPIC.LOCK,
            action: constants_1.LOCK_ACTION.REQUEST,
            name: lockName
        });
    }
    /**
     * Notifies a remote leader keeping a lock that said lock is no longer required
     */
    releaseRemoteLock(lockName) {
        const leaderServerName = this.services.clusterRegistry.getLeader();
        this.services.clusterNode.sendDirect(leaderServerName, {
            topic: constants_1.TOPIC.LOCK,
            action: constants_1.LOCK_ACTION.RELEASE,
            name: lockName
        });
    }
    /**
     * Called when a message is received on the message bus.
     * This could mean the leader responded to a request or that you're currently
     * the leader and received a request.
     */
    onPrivateMessage(message, remoteServerName) {
        if (message.action === constants_1.LOCK_ACTION.RESPONSE) {
            this.handleRemoteLockResponse(message.name, message.locked);
            return;
        }
        if (this.services.clusterRegistry.isLeader() === false) {
            this.services.logger.warn(types_1.EVENT.INVALID_LEADER_REQUEST, `server ${remoteServerName} assumes this node '${this.config.serverName}' is the leader`);
            return;
        }
        if (message.action === constants_1.LOCK_ACTION.REQUEST) {
            this.handleRemoteLockRequest(message.name, remoteServerName);
            return;
        }
        if (message.action === constants_1.LOCK_ACTION.RELEASE) {
            this.releaseLock(message.name);
            return;
        }
    }
    /**
     * Called when a remote lock request is received
     */
    handleRemoteLockRequest(lockName, remoteServerName) {
        this.services.clusterNode.sendDirect(remoteServerName, {
            topic: constants_1.TOPIC.LOCK,
            action: constants_1.LOCK_ACTION.RESPONSE,
            name: lockName,
            locked: this.getLock(lockName)
        });
    }
    /**
     * Called when a remote lock response is received
     */
    handleRemoteLockResponse(lockName, result) {
        clearTimeout(this.timeouts.get(lockName));
        this.timeouts.delete(lockName);
        this.responseEventEmitter.emit(lockName, result);
    }
    /**
     * Returns true if reserving lock was possible otherwise returns false
     */
    getLock(lockName) {
        if (this.locks.has(lockName)) {
            return false;
        }
        this.timeouts.set(lockName, setTimeout(this.onLockTimeout.bind(this, lockName), this.pluginOptions.holdTimeout));
        this.locks.add(lockName);
        return true;
    }
    /**
     * Called when a lock is no longer required and can be released. This is triggered either by
     * a timeout if a remote release message wasn't received in time or when release was called
     * locally.
     *
     * Important note: Anyone can release a lock. It is assumed that the cluster is trusted
     * so maintaining who has the lock is not required. This may need to change going forward.
     */
    releaseLock(lockName) {
        clearTimeout(this.timeouts.get(lockName));
        this.timeouts.delete(lockName);
        this.locks.delete(lockName);
    }
    /**
     * Called when a timeout occurs on a lock that has been reserved for too long
     */
    onLockTimeout(lockName) {
        this.releaseLock(lockName);
        this.services.logger.warn(types_1.EVENT.LOCK_RELEASE_TIMEOUT, `lock ${lockName} released due to timeout`, { lockName });
    }
    /**
     * Called when a remote request has timed out, resulting in notifying the client that
     * the lock wasn't able to be reserved
     */
    onLockRequestTimeout(lockName) {
        this.handleRemoteLockResponse(lockName, false);
        this.services.logger.warn(types_1.EVENT.LOCK_REQUEST_TIMEOUT, `request for lock ${lockName} timed out`, { lockName });
    }
}
exports.DistributedLockRegistry = DistributedLockRegistry;
//# sourceMappingURL=distributed-lock-registry.js.map