mirror of https://github.com/Hypfer/Valetudo.git
294 lines
8.4 KiB
JavaScript
294 lines
8.4 KiB
JavaScript
const EVENT_TYPE = require("./AttributeSubscriber").EVENT_TYPE;
|
|
const Logger = require("../Logger");
|
|
const SerializableEntity = require("./SerializableEntity");
|
|
|
|
/**
|
|
* @typedef {object} AttributeMatcher
|
|
* @property {string} attributeClass
|
|
* @property {string} [attributeType]
|
|
* @property {string} [attributeSubType]
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} AttributeSubscribersMeta
|
|
* @property {AttributeMatcher} matcher
|
|
* @property {Array<import("./AttributeSubscriber")>} subscribers
|
|
*/
|
|
|
|
class ContainerEntity extends SerializableEntity {
|
|
/**
|
|
*
|
|
* @param {object} options
|
|
* @param {Array<import("./Attribute")>} [options.attributes]
|
|
* @param {object} [options.metaData]
|
|
*/
|
|
constructor(options) {
|
|
super(options);
|
|
|
|
this.attributes = options.attributes ?? [];
|
|
|
|
/** @type {Array<AttributeSubscribersMeta>} */
|
|
this.subscribers = [];
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {AttributeMatcher|any} matcher
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.exact]
|
|
* @return {Array<AttributeSubscribersMeta>}
|
|
*/
|
|
getAttributeSubscribersMetas(matcher, options) {
|
|
let exact = false;
|
|
if (options !== undefined && options.exact !== undefined) {
|
|
exact = options.exact;
|
|
}
|
|
return this.subscribers.filter((meta) => {
|
|
if (meta.matcher.attributeClass !== matcher.attributeClass) {
|
|
return false;
|
|
}
|
|
if (((!exact && meta.matcher.attributeType !== undefined) || exact) && meta.matcher.attributeType !== matcher.attributeType) {
|
|
return false;
|
|
}
|
|
// noinspection RedundantIfStatementJS // screw the linter, readability first
|
|
if (((!exact && meta.matcher.attributeSubType !== undefined) || exact) && meta.matcher.attributeSubType !== matcher.attributeSubType) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a new subscription for attribute changes matching the specified matcher.
|
|
*
|
|
* @public
|
|
* @param {import("./AttributeSubscriber")} subscriber
|
|
* @param {AttributeMatcher|any} matcher
|
|
*/
|
|
subscribe(subscriber, matcher) {
|
|
const metas = this.getAttributeSubscribersMetas(matcher, {exact: true});
|
|
let meta;
|
|
|
|
if (metas.length > 0) {
|
|
meta = metas[0];
|
|
if (metas.length > 1) {
|
|
Logger.error("BUG! Found multiple attribute subscribers metas matching exactly. Please report this issue.", this.subscribers);
|
|
}
|
|
} else {
|
|
meta = {
|
|
matcher: matcher,
|
|
subscribers: []
|
|
};
|
|
this.subscribers.push(meta);
|
|
}
|
|
if (!meta.subscribers.includes(subscriber)) {
|
|
meta.subscribers.push(subscriber);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove previously added subscription for attribute changes matching the specified matcher.
|
|
*
|
|
* @public
|
|
* @param {import("./AttributeSubscriber")} subscriber
|
|
* @param {AttributeMatcher|any} matcher
|
|
*/
|
|
unsubscribe(subscriber, matcher) {
|
|
const metas = this.getAttributeSubscribersMetas(matcher, {exact: true});
|
|
if (metas.length > 1) {
|
|
Logger.error("BUG! Found multiple attribute subscribers metas matching exactly. Please report this issue.", this.subscribers);
|
|
}
|
|
this.unsubscribeFromListedMetas(subscriber, metas);
|
|
}
|
|
|
|
/**
|
|
* Remove all subscriptions for specified subscriber.
|
|
*
|
|
* @public
|
|
* @param {import("./AttributeSubscriber")} subscriber
|
|
*/
|
|
unsubscribeAll(subscriber) {
|
|
this.unsubscribeFromListedMetas(subscriber, this.subscribers);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {import("./AttributeSubscriber")} subscriber
|
|
* @param {Array<AttributeSubscribersMeta|any>} metas
|
|
*/
|
|
unsubscribeFromListedMetas(subscriber, metas) {
|
|
const toDelete = [];
|
|
|
|
for (const meta of metas) {
|
|
meta.subscribers = meta.subscribers.filter(registeredSubscriber => {
|
|
return registeredSubscriber !== subscriber;
|
|
});
|
|
|
|
if (meta.subscribers.length === 0) {
|
|
toDelete.push(meta);
|
|
}
|
|
}
|
|
|
|
this.subscribers = this.subscribers.filter(subscriber => {
|
|
return !toDelete.includes(subscriber);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {string} eventType
|
|
* @param {import("./Attribute")|any} attribute
|
|
* @param {import("./Attribute")|any} [previousAttribute]
|
|
*/
|
|
notifySubscribers(eventType, attribute, previousAttribute) {
|
|
const subsMetas = this.getAttributeSubscribersMetas({
|
|
attributeClass: attribute.__class,
|
|
attributeType: attribute.type,
|
|
attributeSubType: attribute.subType
|
|
});
|
|
|
|
for (const meta of subsMetas) {
|
|
for (const subscriber of meta.subscribers) {
|
|
subscriber.onAttributeEvent(eventType, attribute, previousAttribute);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*
|
|
* @param {AttributeMatcher|any} options
|
|
*/
|
|
hasMatchingAttribute(options) {
|
|
return this.getMatchingAttributes(options).length > 0;
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*
|
|
* @param {AttributeMatcher|any} options
|
|
* @returns {Array<any|import("./Attribute")>}
|
|
*/
|
|
getMatchingAttributes(options) {
|
|
let needles = this.attributes.filter(e => {
|
|
return e.constructor.name === options.attributeClass;
|
|
});
|
|
|
|
if (options.attributeType) {
|
|
needles = needles.filter(e => {
|
|
return e.type === options.attributeType;
|
|
});
|
|
|
|
if (options.attributeSubType) {
|
|
return needles.filter(e => {
|
|
return e.subType === options.attributeSubType;
|
|
});
|
|
} else {
|
|
return needles;
|
|
}
|
|
} else {
|
|
return needles;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*
|
|
* @param {AttributeMatcher|any} options
|
|
* @returns {void}
|
|
*/
|
|
removeMatchingAttributes(options) {
|
|
let needles = this.getMatchingAttributes(options);
|
|
this.attributes = this.attributes.filter(e => {
|
|
return !needles.includes(e);
|
|
});
|
|
|
|
for (const attr of needles) {
|
|
this.notifySubscribers(EVENT_TYPE.DELETE, attr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*
|
|
* @param {AttributeMatcher|any} options
|
|
* @returns {any|import("./Attribute")}
|
|
*/
|
|
getFirstMatchingAttribute(options) {
|
|
const index = this.getFirstMatchingAttributeIndex(options);
|
|
|
|
if (index !== -1) {
|
|
return this.attributes[index];
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*
|
|
* @param {Function} ctor
|
|
* @returns {any|import("./Attribute")}
|
|
*/
|
|
getFirstMatchingAttributeByConstructor(ctor) {
|
|
return this.getFirstMatchingAttribute({attributeClass: ctor.name});
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*
|
|
* @param {AttributeMatcher} options
|
|
* @returns {number}
|
|
*/
|
|
getFirstMatchingAttributeIndex(options) {
|
|
return this.attributes.findIndex(e => {
|
|
let matches = e.constructor.name === options.attributeClass;
|
|
|
|
if (options.attributeType && matches) {
|
|
matches = e.type === options.attributeType;
|
|
}
|
|
|
|
if (options.attributeSubType && matches) {
|
|
matches = e.subType === options.attributeSubType;
|
|
}
|
|
|
|
return matches;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {object} newAttribute
|
|
*/
|
|
upsertFirstMatchingAttribute(newAttribute) {
|
|
const index = this.getFirstMatchingAttributeIndex({
|
|
attributeClass: newAttribute.__class,
|
|
attributeType: newAttribute.type,
|
|
attributeSubType: newAttribute.subType
|
|
});
|
|
|
|
if (index === -1) {
|
|
this.attributes.push(newAttribute);
|
|
this.notifySubscribers(EVENT_TYPE.ADD, newAttribute);
|
|
|
|
return null;
|
|
} else {
|
|
const previousAttribute = this.attributes[index];
|
|
|
|
this.attributes[index] = newAttribute;
|
|
this.notifySubscribers(EVENT_TYPE.CHANGE, newAttribute, previousAttribute);
|
|
|
|
return previousAttribute;
|
|
}
|
|
}
|
|
|
|
toJSON() {
|
|
return {
|
|
__class: this.__class,
|
|
metaData: this.metaData,
|
|
attributes: this.attributes
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = ContainerEntity;
|