Source: components/MonitorDataComponent.js

import { Utils } from "../common/Utils";
import { BaseComponent } from "../components/BaseComponent";

const __ = {
	private: Symbol('private'),
}

// Ignored attribute changes.
let ignoreChangingAttributes = ['_object', '_active', 'app'];

/**
 * @class MonitorDataComponent
 * The monitor data component.
 * @memberof THING
 * @extends THING.BaseComponent
 * @public
 */
class MonitorDataComponent extends BaseComponent {

	constructor() {
		super();

		this[__.private] = {};
		let _private = this[__.private];

		_private.anyPendingChanges = {};

		_private.pendingChangeCount = 0;
		_private.pendingChanges = {};
		_private.pendingSubscribers = [];

		_private.changedCount = 0;

		_private.monitorAttributesNames = [];
		_private.monitorAttributes = {};

		// subscription list
		_private._subscribers = new Map();

		// Callback when changing attributes.
		const changePropValue = (object, prop, value, receiver) => {
			// If it is an attribute that needs to ignore changes, do not proxy.
			if (ignoreChangingAttributes.includes(prop)) {
				return true;
			}

			_private.monitorAttributes[prop] = value;

			if (_private.monitorAttributesNames.includes('*')) {
				if (!_private.pendingChangeCount) {
					// If it is a single attribute change and subscribed to a callback for any attribute change.
					_private.anyPendingChanges = { [prop]: value };
				}
				else {
					if (object[prop] == value) {
						delete _private.anyPendingChanges[prop];
					}
					else {
						_private.anyPendingChanges[prop] = value;
					}
				}

				if (!_private.monitorAttributesNames.includes(prop)) {
					_private.monitorAttributesNames.push(prop);
				}
			}

			object[prop] = value;

			if (_private.monitorAttributesNames.includes(prop)) {
				// If there are multiple attribute changes.
				if (_private.pendingChangeCount) {
					_private.pendingChanges[prop] = value;
					_private.changedCount++;
					if (_private.changedCount == _private.pendingChangeCount) {
						this._publish(prop, value);
					}
				}
				else {
					this._publish(prop, value);
				}
			}
		}

		// Proxy the set and deleteProperty interfaces of the class.
		let proxy = new Proxy(this, {
			set: function (object, prop, value, receiver) {
				changePropValue(object, prop, value, receiver);
				return true;
			},
			deleteProperty: function (object, prop) {
				changePropValue(object, prop);
				delete object[prop];
				return true;
			}
		});

		// Proxy monitorAttributes to this.
		Object.defineProperty(proxy, 'customFormatters', {
			enumerable: false,
			configurable: false,
			get: function () {
				return ['object', { object: _private.monitorAttributes }];
			}
		});

		return proxy;
	}

	setAttributes(name, value) {
		let _private = this[__.private];

		if (Utils.isString(name)) {
			this[name] = value;
		}
		else if (Utils.isObject(name)) {
			if (!_private.monitorAttributesNames.includes('*')) {
				const intersection = Object.keys(name).filter(value => _private.monitorAttributesNames.includes(value));
				_private.pendingChangeCount = intersection.length;
			}
			else {
				_private.pendingChangeCount = Object.keys(name).length;
			}

			_private.changedCount = 0;
			for (let prop in name) {
				this[prop] = name[prop];
			}
		}
	}

	subscribe(key, fn) {
		let _private = this[__.private];

		// Monitor all data changes.
		if (Utils.isFunction(key)) {
			fn = key;
			key = '*';
		}
		// Monitoring partial data changes.
		else if (Utils.isArray(key)) {
			_private.pendingSubscribers = [];
			for (let i = 0; i < key.length; i++) {
				_private.pendingSubscribers.push(this.subscribe(key[i], fn));
			}
			return _private.pendingSubscribers;
		}

		// Add to monitoring attribute array.
		if (!_private.monitorAttributesNames.includes(key)) {
			_private.monitorAttributesNames.push(key);
		}

		return this._addSubscriber(key, fn);
	}

	_addSubscriber(key, fn) {
		let _private = this[__.private];

		if (Utils.isFunction(fn)) {
			let arr = _private._subscribers.get(key);
			if (arr) {
				arr.push(fn);
			}
			else {
				_private._subscribers.set(key, [fn])
			}

			return { key, listener: fn };
		}
		else {
			console.warn('Subscription callback should be a function.');
		}
	}

	unsubscribe(subscriber) {
		if (Utils.isArray(subscriber)) {
			subscriber.forEach(element =>
				this._unsubscribe(element.key, element.listener)
			);
		}
		else {
			this._unsubscribe(subscriber.key, subscriber.listener)
		}
	}

	_unsubscribe(key, fn) {
		let _private = this[__.private];
		let fns = _private._subscribers.get(key);
		if (fns && fns.length > 0) {
			for (let i = fns.length - 1; i >= 0; i--) {
				if (fns[i] == fn) {
					fns.splice(i, 1);
				}
			}
		}
	}

	unsubscribeAll() {
		let _private = this[__.private];
		_private._subscribers.clear();
	}

	clearAttributes() {
		let _private = this[__.private];
		for (let prop in _private.monitorAttributes) {
			delete this[prop];
		}
		_private.monitorAttributes = {};
	}

	_publish(prop, value) {
		let _private = this[__.private];

		// If the callback contains any attribute changes.
		let anyFns = _private._subscribers.get('*');
		if (anyFns) {
			anyFns.forEach((fn) => {
				fn({ data: _private.anyPendingChanges });
			})
		}

		// If the number of pending changes is greater than 0, only messages that are subscribed to together with these attributes will be published.
		if (_private.pendingChangeCount > 0) {
			let commonFns = this._findCommonValues(_private._subscribers, Object.keys(_private.pendingChanges));
			if (commonFns.length > 0) {
				commonFns.forEach(function (fn) {
					fn({ data: _private.pendingChanges });
				})
			}
		}
		else {
			// Get the function by prop and trigger these functions.
			let fns = _private._subscribers.get(prop);
			if (!fns || fns.length === 0) {
				return;
			}
			for (let i = 0; i < fns.length; i++) {
				fns[i]({ data: { [prop]: value }});
			}
		}

		this._clearPendingChanges();
	}

	_clearPendingChanges() {
		let _private = this[__.private];

		// Clear pending attribute changes.
		_private.anyPendingChanges = {};

		_private.pendingChanges = {};
		_private.pendingChangeCount = 0;
		_private.changedCount = 0;
	}

	_findCommonValues(map, keys) {
		if (!Array.isArray(keys) || keys.length === 0) {
			return undefined;
		}

		// Find the callback corresponding to the monitored attribute.
		const valueArrays = keys.map(
			key => map.get(key)
		);

		// Using the reduce () method to find the same value.
		const commonValues = valueArrays.reduce((intersection, currentArray) => {
			if (!intersection || intersection.length === 0) {
				return currentArray;
			}
			return intersection.filter(value => currentArray.includes(value));
		}, []);

		// return commonValues;
		return [...new Set(commonValues)];
	}

}

export { MonitorDataComponent }