"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    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) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LockAttributes = void 0;
const Errors_js_1 = require("./Errors.js");
const EventEmitter_js_1 = __importStar(require("./utilities/EventEmitter.js"));
const SpaceUpdate_js_1 = __importDefault(require("./SpaceUpdate.js"));
class LockAttributes extends Map {
    toJSON() {
        return Object.fromEntries(this);
    }
}
exports.LockAttributes = LockAttributes;
class Locks extends EventEmitter_js_1.default {
    constructor(space, presenceUpdate) {
        super();
        this.space = space;
        this.presenceUpdate = presenceUpdate;
        this.locks = new Map();
    }
    get(id) {
        const locks = this.locks.get(id);
        if (!locks)
            return;
        for (const lock of locks.values()) {
            if (lock.status === 'locked') {
                return lock;
            }
        }
    }
    // This will be async in the future, when pending requests are no longer processed
    // in the library.
    getAll() {
        return __awaiter(this, void 0, void 0, function* () {
            const allLocks = [];
            for (const locks of this.locks.values()) {
                for (const lock of locks.values()) {
                    if (lock.status === 'locked') {
                        allLocks.push(lock);
                    }
                }
            }
            return allLocks;
        });
    }
    getSelf() {
        return __awaiter(this, void 0, void 0, function* () {
            const self = yield this.space.members.getSelf();
            if (!self)
                return [];
            return this.getLocksForConnectionId(self.connectionId).filter((lock) => lock.status === 'locked');
        });
    }
    getOthers() {
        return __awaiter(this, void 0, void 0, function* () {
            const self = yield this.space.members.getSelf();
            const allLocks = yield this.getAll();
            if (!self)
                return allLocks;
            return allLocks.filter((lock) => lock.member.connectionId !== self.connectionId);
        });
    }
    acquire(id, opts) {
        return __awaiter(this, void 0, void 0, function* () {
            const self = yield this.space.members.getSelf();
            if (!self) {
                throw (0, Errors_js_1.ERR_NOT_ENTERED_SPACE)();
            }
            // check there isn't an existing PENDING or LOCKED request for the current
            // member, since we do not support nested locks
            let lock = this.getLock(id, self.connectionId);
            if (lock && lock.status !== 'unlocked') {
                throw (0, Errors_js_1.ERR_LOCK_REQUEST_EXISTS)();
            }
            // initialise a new PENDING request
            lock = {
                id,
                status: 'pending',
                timestamp: Date.now(),
                member: self,
            };
            if (opts) {
                lock.attributes = opts.attributes;
            }
            this.setLock(lock);
            // reflect the change in the member's presence data
            yield this.updatePresence(self);
            return lock;
        });
    }
    release(id) {
        return __awaiter(this, void 0, void 0, function* () {
            const self = yield this.space.members.getSelf();
            if (!self) {
                throw (0, Errors_js_1.ERR_NOT_ENTERED_SPACE)();
            }
            const lock = this.getLock(id, self.connectionId);
            if (!lock)
                return;
            lock.status = 'unlocked';
            lock.reason = undefined;
            // Send presence update with the updated lock, but delete afterwards so when the
            // message is processed an update event is fired
            this.updatePresence(self);
            this.deleteLock(id, self.connectionId);
        });
    }
    subscribe(listenerOrEvents, listener) {
        try {
            super.on(listenerOrEvents, listener);
        }
        catch (e) {
            if (e instanceof EventEmitter_js_1.InvalidArgumentError) {
                throw new EventEmitter_js_1.InvalidArgumentError('Locks.subscribe(): Invalid arguments: ' + (0, EventEmitter_js_1.inspect)([listenerOrEvents, listener]));
            }
            else {
                throw e;
            }
        }
    }
    unsubscribe(listenerOrEvents, listener) {
        try {
            super.off(listenerOrEvents, listener);
        }
        catch (e) {
            if (e instanceof EventEmitter_js_1.InvalidArgumentError) {
                throw new EventEmitter_js_1.InvalidArgumentError('Locks.unsubscribe(): Invalid arguments: ' + (0, EventEmitter_js_1.inspect)([listenerOrEvents, listener]));
            }
            else {
                throw e;
            }
        }
    }
    processPresenceMessage(message) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            const member = yield this.space.members.getByConnectionId(message.connectionId);
            if (!member)
                return;
            if (message.action === 'leave' || !Array.isArray((_a = message === null || message === void 0 ? void 0 : message.extras) === null || _a === void 0 ? void 0 : _a.locks)) {
                // the member has left, or they have no locks in presence, so release any
                // existing locks for that member
                for (const locks of this.locks.values()) {
                    const lock = locks.get(member.connectionId);
                    if (lock) {
                        lock.status = 'unlocked';
                        lock.reason = undefined;
                        this.emit('update', lock);
                        locks.delete(member.connectionId);
                    }
                }
                return;
            }
            message.extras.locks.forEach((lock) => {
                const existing = this.getLock(lock.id, member.connectionId);
                // special-case the handling of PENDING requests, which will eventually
                // be done by the Ably system, at which point this can be removed
                if (lock.status === 'pending' && (!existing || existing.status === 'pending')) {
                    this.processPending(member, lock);
                }
                if (!existing || existing.status !== lock.status) {
                    this.emit('update', Object.assign(Object.assign({}, lock), { member }));
                }
                this.setLock(Object.assign(Object.assign({}, lock), { member }));
            });
            // handle locks which have been unlocked and longer need to be held locally
            for (const locks of this.locks.values()) {
                for (const lock of locks.values()) {
                    if (lock.status === 'unlocked') {
                        this.deleteLock(lock.id, lock.member.connectionId);
                    }
                }
            }
        });
    }
    // process a PENDING lock request by determining whether it should be
    // considered LOCKED or UNLOCKED with a reason, potentially invalidating
    // existing LOCKED requests.
    //
    // TODO: remove this once the Ably system processes PENDING requests
    //       internally using this same logic.
    processPending(member, pendingLock) {
        // if the requested lock ID is not currently locked, then mark the PENDING
        // lock request as LOCKED
        const lock = this.get(pendingLock.id);
        if (!lock) {
            pendingLock.status = 'locked';
            return;
        }
        // check if the PENDING lock should invalidate the existing LOCKED request.
        //
        // This is necessary because presence data is eventually consistent, so
        // there's no guarantee that all members see presence messages in the same
        // order, which could lead to members not agreeing which members hold which
        // locks.
        //
        // For example, if two members both request the same lock at roughly the
        // same time, and both members see their own request in presence before
        // seeing the other's request, then they will each consider themselves to
        // hold the lock.
        //
        // To minimise the impact of this propagation issue, a further check is
        // made allowing a PENDING request to invalidate an existing LOCKED request
        // if the PENDING request has a timestamp which predates the LOCKED
        // request, or, if the timestamps are the same, if the PENDING request has
        // a connectionId which sorts lexicographically before the connectionId of
        // the LOCKED request.
        if (pendingLock.timestamp < lock.timestamp ||
            (pendingLock.timestamp == lock.timestamp && member.connectionId < lock.member.connectionId)) {
            pendingLock.status = 'locked';
            lock.status = 'unlocked';
            lock.reason = (0, Errors_js_1.ERR_LOCK_INVALIDATED)();
            this.emit('update', lock);
            return;
        }
        // the lock is LOCKED and the PENDING request did not invalidate it, so
        // mark the PENDING request as UNLOCKED with a reason.
        pendingLock.status = 'unlocked';
        pendingLock.reason = (0, Errors_js_1.ERR_LOCK_IS_LOCKED)();
    }
    updatePresence(self) {
        const update = new SpaceUpdate_js_1.default({ self, extras: this.getLockExtras(self.connectionId) });
        return this.presenceUpdate(update.noop());
    }
    getLock(id, connectionId) {
        const locks = this.locks.get(id);
        if (!locks)
            return;
        return locks.get(connectionId);
    }
    setLock(lock) {
        let locks = this.locks.get(lock.id);
        if (!locks) {
            locks = new Map();
            this.locks.set(lock.id, locks);
        }
        locks.set(lock.member.connectionId, lock);
    }
    deleteLock(id, connectionId) {
        const locks = this.locks.get(id);
        if (!locks)
            return;
        return locks.delete(connectionId);
    }
    getLocksForConnectionId(connectionId) {
        const requests = [];
        for (const locks of this.locks.values()) {
            const lock = locks.get(connectionId);
            if (lock) {
                requests.push(lock);
            }
        }
        return requests;
    }
    getLockExtras(connectionId) {
        const locks = this.getLocksForConnectionId(connectionId);
        if (locks.length === 0)
            return;
        return { locks };
    }
}
exports.default = Locks;
//# sourceMappingURL=Locks.js.map