Home Reference Source

src/chat/AckCounter.js

/** @private */
export class AckCounter {
	constructor() {
		this.limit = 2;
		this.data = [];
		this.lastAckedBy = [];
		this.olderAcked = false;
	}

	/**
	 * Sets the number of times an element needs to be incremented to be removed
	 * @param {number} limit
	 */
	setLimit(limit) {
		this.limit = limit;
	}

	/**
	 * Adds a new element at the end of the collection.
	 * @param {string} element
	 * @return {boolean} true if the element is correctly added, false otherwise
	 */
	append(element) {
		if (this.data.find(d => d.key === element)) {
			return false;
		}
		this.data.push({key: element, value: new window.Set()});
		return true;
	}

	/**
	 * Adds a new element at the start of the collection.
	 * @param {string} element
	 */
	prepend(element) {
		if (this.olderAcked) {
			return;
		}
		// If element has received no acks, copy last added element
		if (!this.data.find(d => d.key === element)) {
			for (let i = this.data.length - 1; i >= 0; i--) {
				if (this.data[i].isAdded) {
					const value = new window.Set(this.data[i].value);
					this.data.splice(i, 0, {key: element, value});
					break;
				}
			}
		}
		// If there was no last added element, add new element as the newest element
		if (!this.data.find(d => d.key === element)) {
			this.data.push({key: element, value: new window.Set()});
		}

		const index = this.data.findIndex(({key}) => key === element);
		this.data[index].isAdded = true;
		const {value} = this.data[index];
		this.olderAcked = value.size >= this.limit;
	}

	/**
	 * Allows knowing if the specified element is currently in the collection
	 * @param {string} element
	 * @return {boolean}
	 */
	hasElement(element) {
		return !!this.data.find(d => d.key === element);
	}

	/**
	 * Adds a new ack to the specified element from the specified user
	 * @param {string} element
	 * @param {string} user
	 * @returns {boolean} true if the element reached the limit, false otherwise
	 */
	appendAck(element, user) {
		if (!this.data.find(d => d.key === element)) {
			throw new Error(`${element} doesn't exist`);
		}
		// Add user until key is found
		for (const {key, value} of this.data) {
			value.add(user);
			if (key === element) {
				break;
			}
		}
		// When limit is reached, remove every element until the one that reach the limit
		let keyAcked = false;
		for (let i = this.data.length - 1; i >= 0; i--) {
			const value = this.data[i].value;
			const key = this.data[i].key;
			if (!keyAcked && value.size === this.limit) {
				keyAcked = key;
				this.olderAcked = true; // We can assure any older message is already acked
			}
			if (keyAcked) {
				this.data.splice(i, 1);
			}
		}
		this.lastAckedBy[user] = element;
		return keyAcked;
	}

	prependAck(element, user) {
		if (this.olderAcked) {
			return;
		}
		// If it is the first ACK for an element, copy oldest one
		if (!this.data.find(d => d.key === element)) {
			const prevValue = new window.Set(this.data[0] && this.data[0].value);
			this.data.unshift({key: element, value: prevValue});
		}
		// Add user until key is found
		for (const {key, value} of this.data) {
			value.add(user);
			if (key === element) {
				break;
			}
		}
		if (!this.lastAckedBy[user]) {
			this.lastAckedBy[user] = element;
		}
	}

	isOlderAcked() {
		return this.olderAcked;
	}

	getLastAckedBy(user) {
		return this.lastAckedBy[user];
	}
}