"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 = require("./logging");
const p_queue_1 = __importDefault(require("p-queue"));
const log = logging_1.get("MembershipQueue");
const DEFAULT_OPTS = {
    concurrentRoomLimit: 8,
    maxAttempts: 10,
    joinDelayMs: 500,
    maxJoinDelayMs: 30 * 60 * 1000,
};
/**
 * This class sends membership changes for rooms in a linearized queue.
 */
class MembershipQueue {
    constructor(bridge, opts) {
        this.bridge = bridge;
        this.opts = opts;
        this.queues = new Map();
        this.opts = { ...DEFAULT_OPTS, ...this.opts };
        for (let i = 0; i < this.opts.concurrentRoomLimit; i++) {
            this.queues.set(i, new p_queue_1.default({
                autoStart: true,
                concurrency: 1,
            }));
        }
    }
    /**
     * 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: userId || this.bridge.botUserId,
            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: userId || this.bridge.botUserId,
            retry,
            req,
            attempts: 0,
            reason,
            kickUser,
            type: "leave",
        });
    }
    async queueMembership(item) {
        try {
            const queue = this.queues.get(this.hashRoomId(item.roomId));
            if (!queue) {
                throw Error("Could not find queue for hash");
            }
            queue.add(() => this.serviceQueue(item));
        }
        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)
            % this.opts.concurrentRoomLimit;
    }
    async serviceQueue(item) {
        const { req, roomId, userId, reason, kickUser, attempts, type } = item;
        const reqIdStr = req.getId() ? `[${req.getId()}]` : "";
        log.debug(`${reqIdStr} ${userId}@${roomId} -> ${type} (reason: ${reason || "none"}, kicker: ${kickUser})`);
        const intent = this.bridge.getIntent(kickUser || userId);
        try {
            if (type === "join") {
                await intent.join(roomId);
            }
            else if (kickUser) {
                await intent.kick(roomId, userId, reason);
            }
            else {
                await intent.leave(roomId, reason);
            }
        }
        catch (ex) {
            if (!this.shouldRetry(ex, attempts)) {
                throw ex;
            }
            const delay = Math.min((this.opts.joinDelayMs * attempts) + (Math.random() * 500), this.opts.maxJoinDelayMs);
            log.warn(`${reqIdStr} Failed to ${type} ${roomId}, delaying for ${delay}ms`);
            log.debug(`${reqIdStr} Failed with: ${ex.errcode} ${ex.message}`);
            await new Promise((r) => setTimeout(r, delay));
            this.queueMembership({ ...item, attempts: item.attempts + 1 });
        }
    }
    shouldRetry(ex, attempts) {
        return !(attempts === this.opts.maxAttempts ||
            ex.errcode === "M_FORBIDDEN" ||
            ex.httpStatus === 403);
    }
}
exports.MembershipQueue = MembershipQueue;
//# sourceMappingURL=membership-queue.js.map