"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MembershipQueue = void 0;
const logging_1 = __importDefault(require("../logging"));
const QueuePool_1 = require("./QueuePool");
const log = logging_1.default("MembershipQueue");
const CONCURRENT_ROOM_LIMIT = 8;
const ATTEMPTS_LIMIT = 10;
const JOIN_DELAY_MS = 500;
const JOIN_DELAY_CAP_MS = 30 * 60 * 1000; // 30 mins
/**
 * This class processes membership changes for rooms in a linearized queue.
 */
class MembershipQueue {
    constructor(bridge) {
        this.bridge = bridge;
        this.queuePool = new QueuePool_1.QueuePool(CONCURRENT_ROOM_LIMIT, this.serviceQueue.bind(this));
    }
    /**
     * Join a user to a room
     * @param roomId The roomId to join
     * @param userId Leave empty to act as the bot user.
     * @param req The request entry for logging context
     * @param retry Should the request retry if it fails
     */
    async join(roomId, userId, req, retry = true) {
        return this.queueMembership({
            roomId,
            userId,
            retry,
            req,
            attempts: 0,
            type: "join",
        });
    }
    /**
     * Leave OR kick a user from a room
     * @param roomId The roomId to leave
     * @param userId Leave empty to act as the bot user.
     * @param req The request entry for logging context
     * @param retry Should the request retry if it fails
     * @param reason Reason for leaving/kicking
     * @param kickUser The user to be kicked. If left blank, this will be a leave.
     */
    async leave(roomId, userId, req, retry = true, reason, kickUser) {
        return this.queueMembership({
            roomId,
            userId,
            retry,
            req,
            attempts: 0,
            reason,
            kickUser,
            type: "leave",
        });
    }
    async queueMembership(item) {
        try {
            return await this.queuePool.enqueue("", item, this.hashRoomId(item.roomId));
        }
        catch (ex) {
            log.error(`Failed to handle membership: ${ex}`);
            throw ex;
        }
    }
    hashRoomId(roomId) {
        return Array.from(roomId).map((s) => s.charCodeAt(0)).reduce((a, b) => a + b, 0) % CONCURRENT_ROOM_LIMIT;
    }
    async serviceQueue(item) {
        log.debug(`${item.userId}@${item.roomId} -> ${item.type}`);
        const { req, roomId, userId, reason, kickUser, attempts } = item;
        const intent = this.bridge.getIntent(kickUser || userId);
        try {
            if (item.type === "join") {
                await intent.join(roomId);
            }
            else {
                await intent[kickUser ? "kick" : "leave"](roomId, userId || "", reason);
            }
        }
        catch (ex) {
            if (!this.shouldRetry(ex, attempts)) {
                throw ex;
            }
            const delay = Math.min((JOIN_DELAY_MS * attempts) + (Math.random() * 500), JOIN_DELAY_CAP_MS);
            req.log.warn(`Failed to join ${roomId}, delaying for ${delay}ms`);
            req.log.debug(`Failed with: ${ex.errcode} ${ex.message}`);
            await new Promise((r) => setTimeout(r, delay));
            this.queueMembership(Object.assign(Object.assign({}, item), { attempts: item.attempts + 1 }));
        }
    }
    shouldRetry(ex, attempts) {
        return !(attempts === ATTEMPTS_LIMIT ||
            ex.errcode === "M_FORBIDDEN" ||
            ex.httpStatus === 403);
    }
}
exports.MembershipQueue = MembershipQueue;
//# sourceMappingURL=MembershipQueue.js.map