import { Utils } from "../common/Utils";
import { BaseComponent } from "../components/BaseComponent";
const __ = {
private: Symbol('private'),
}
// Ignored attribute changes.
let ignoreChangingAttributes = ['_object', '_active', 'app'];
/**
* @class MonitorDataComponent
* 监控数据组件。
* @example
* // 订阅任何数据变化
* let watcher1 = obj.monitorData.subscribe((ev) => {
* console.log(ev.data); // 变化的数据
* console.log(obj.monitorData); // 完整的数据
* });
*
* // 订阅特定数据变化
* let watcher2 = obj.monitorData.subscribe(["power", "freq"], (ev) => {
* console.log(ev.data["power"]);
* console.log(ev.data["freq"]);
* });
*
* // 订阅单个数据变化
* let watcher3 = obj.monitorData.subscribe("power", (ev) => {
* console.log(ev.data["power"]);
* });
*
* // 获取监控属性
* let power = obj.monitorData["power"];
* let freq = obj.monitorData["freq"];
*
* // 设置监控属性
* obj.monitorData.setAttributes({
* "power": 3200,
* "brand": "ant",
* "freq": 95
* });
*
* // 取消特定订阅
* obj.monitorData.unsubscribe(watcher1);
*
* // 取消所有订阅
* obj.monitorData.unsubscribeAll();
*
* // 清空所有属性(不会取消订阅)
* obj.monitorData.clearAttributes();
* @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;
}
// Store the original value for comparison
const originalValue = object[prop];
// Check if the value has actually changed
const hasChanged = originalValue !== value;
_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.
if (hasChanged) {
_private.anyPendingChanges = { [prop]: value };
}
}
else {
if (!hasChanged) {
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) {
// Only add to pendingChanges if the value has actually changed
if (hasChanged) {
_private.pendingChanges[prop] = value;
}
_private.changedCount++;
if (_private.changedCount == _private.pendingChangeCount) {
// Push all pending changes at once
// Use anyPendingChanges for subscribers to all changes
if (_private.monitorAttributesNames.includes('*')) {
_private.app.monitorManager.push(_private.object, '*', _private.anyPendingChanges);
} else {
_private.app.monitorManager.push(_private.object, '*', _private.pendingChanges);
}
this._clearPendingChanges();
}
}
else {
// Single property change - only push if the value has actually changed
if (hasChanged) {
_private.app.monitorManager.push(_private.object, 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;
}
/**
* 当组件被添加到对象时调用
* @param {object} object - 组件被添加到的对象
*/
onAdd(object) {
// 不能调用父类接口,因为子类代理了属性操作
// super.onAdd(object);
let _private = this[__.private];
_private.object = object;
_private.app = object.isApp ? object : object.app;
}
/**
* 设置一个或多个属性
* @param {string|object} name - 属性名或包含多个属性的对象
* @param {*} [value] - 如果name是字符串,则为要设置的值
* @example
* // 设置单个属性
* obj.monitorData.setAttributes("power", 3200);
*
* // 设置多个属性
* obj.monitorData.setAttributes({
* power: 3200,
* freq: 95,
* brand: "ant"
* });
*
* // 使用路径设置嵌套属性
* obj.monitorData.setAttributes("status/power", 3200);
* @public
*/
setAttributes(name, value) {
let _private = this[__.private];
if (Utils.isString(name)) {
// Handle path-style attribute setting
if (name.includes('/')) {
const parts = name.split('/');
let current = this;
// Create nested objects along the path
for (let i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]]) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
// Set the final value
current[parts[parts.length - 1]] = value;
}
else {
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) {
// Support path-style keys in object
if (prop.includes('/')) {
this.setAttributes(prop, name[prop]);
}
else {
this[prop] = name[prop];
}
}
}
}
/**
* 订阅属性变化
* @param {string|string[]|Function} key - 要订阅的属性名(可以是数组),或者用于所有变化的回调函数
* @param {Function} [fn] - 如果key不是函数,则为回调函数
* @returns {object|object[]} 订阅对象或订阅对象数组
* @example
* // 订阅所有变化
* let watcher1 = obj.monitorData.subscribe((ev) => {
* console.log(ev.data); // 变化的数据
* });
*
* // 订阅多个属性
* let watcher2 = obj.monitorData.subscribe(["power", "freq"], (ev) => {
* console.log(ev.data.power);
* console.log(ev.data.freq);
* });
*
* // 订阅单个属性
* let watcher3 = obj.monitorData.subscribe("power", (ev) => {
* console.log(ev.data.power);
* });
* @public
*/
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)) {
// Create a unique key for this array subscription
const arrayKey = JSON.stringify(key.sort());
// Add each property to the monitoring attribute array
key.forEach(propKey => {
if (!_private.monitorAttributesNames.includes(propKey)) {
_private.monitorAttributesNames.push(propKey);
}
});
// Store the array of properties with this subscriber
const subscriber = this._addSubscriber(arrayKey, fn);
subscriber.isArraySubscription = true;
subscriber.properties = key;
return subscriber;
}
// 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.');
return null;
}
}
/**
* 取消订阅属性变化
* @param {object|object[]} subscriber - 由subscribe()返回的订阅对象或订阅对象数组
* @example
* // 取消单个订阅
* obj.monitorData.unsubscribe(watcher1);
*
* // 取消多个订阅
* obj.monitorData.unsubscribe([watcher1, watcher2]);
* @public
*/
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);
}
}
}
}
/**
* 取消所有属性变化的订阅
* @example
* // 取消所有订阅
* obj.monitorData.unsubscribeAll();
* @public
*/
unsubscribeAll() {
let _private = this[__.private];
_private._subscribers.clear();
}
/**
* 清除所有被监控的属性
* @example
* // 清除所有监控属性
* obj.monitorData.clearAttributes();
* @public
*/
clearAttributes() {
let _private = this[__.private];
for (let prop in _private.monitorAttributes) {
delete this[prop];
}
_private.monitorAttributes = {};
}
_publish(prop, value) {
let _private = this[__.private];
// Track which callbacks have been called to prevent duplicates
const calledCallbacks = new Set();
// If prop is '*', it means we're publishing all changes at once
if (prop === '*' && typeof value === 'object') {
this._publishMultipleChanges(value, calledCallbacks);
} else {
this._publishSingleChange(prop, value, calledCallbacks);
}
this._clearPendingChanges();
}
/**
* 发布多个属性变化事件
* @param {object} changes - 变化的属性对象
* @param {Set} calledCallbacks - 已调用的回调函数集合
* @private
*/
_publishMultipleChanges(changes, calledCallbacks) {
let _private = this[__.private];
// 前面已经过滤了未变化的属性,这里直接使用
const changedProps = Object.keys(changes).filter(key => key !== '*');
// Only proceed if there are actually changed properties
if (changedProps.length > 0) {
// 处理订阅所有变化的回调
this._notifyAllChangesSubscribers(changes, calledCallbacks);
// 处理数组订阅
this._notifyArraySubscribers(changedProps, changes, calledCallbacks);
// 处理单属性订阅
this._notifyPropertySubscribers(changedProps, changes, calledCallbacks);
}
}
/**
* 发布单个属性变化事件
* @param {string} prop - 变化的属性名
* @param {*} value - 变化的属性值
* @param {Set} calledCallbacks - 已调用的回调函数集合
* @private
*/
_publishSingleChange(prop, value, calledCallbacks) {
let _private = this[__.private];
// 处理订阅所有变化的回调
this._notifyAllChangesSubscribers(_private.anyPendingChanges, calledCallbacks);
// 处理数组订阅
this._notifyArraySubscribersForSingleProp(prop, value, calledCallbacks);
// 处理多属性变化
this._notifyMultiPropSubscribers(calledCallbacks);
// 处理单属性订阅
this._notifyPropertySubscriber(prop, value, calledCallbacks);
}
_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 [];
}
// Filter out '*' from keys if present
const filteredKeys = keys.filter(key => key !== '*');
// If all keys were '*', return an empty array
if (filteredKeys.length === 0) {
return [];
}
// Find the callback corresponding to the monitored attribute.
const valueArrays = filteredKeys.map(
key => map.get(key)
).filter(array => array && array.length > 0); // Filter out undefined or empty arrays
// If no valid arrays found, return empty array
if (valueArrays.length === 0) {
return [];
}
// 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)];
}
_notifyAllChangesSubscribers(changes, calledCallbacks) {
let _private = this[__.private];
// For subscribers to all changes
let anyFns = _private._subscribers.get('*');
if (anyFns && anyFns.length > 0) {
this._notifySubscribers(anyFns, changes, calledCallbacks);
}
}
_notifyArraySubscribers(changedProps, changes, calledCallbacks) {
let _private = this[__.private];
// For array subscriptions, check if any of the properties in the array have changed
_private._subscribers.forEach((fns, key) => {
// Skip if this is not a JSON string (not an array subscription)
if (!key.startsWith('[') || !key.endsWith(']')) return;
try {
// Parse the array key
const properties = JSON.parse(key);
// Check if any of the properties in this subscription have changed
const relevantChanges = {};
let hasRelevantChanges = false;
properties.forEach(prop => {
if (changedProps.includes(prop)) {
relevantChanges[prop] = changes[prop];
hasRelevantChanges = true;
}
});
// If there are relevant changes, notify subscribers
if (hasRelevantChanges) {
this._notifySubscribers(fns, relevantChanges, calledCallbacks);
}
} catch (e) {
// Ignore parsing errors
}
});
}
_notifyPropertySubscribers(changedProps, changes, calledCallbacks) {
let _private = this[__.private];
// For subscribers to specific properties
changedProps.forEach(changedProp => {
let propFns = _private._subscribers.get(changedProp);
if (propFns && propFns.length > 0) {
this._notifySubscribers(propFns, { [changedProp]: changes[changedProp] }, calledCallbacks);
}
});
}
_notifyArraySubscribersForSingleProp(prop, value, calledCallbacks) {
let _private = this[__.private];
// Check array subscriptions that include this property
_private._subscribers.forEach((fns, key) => {
// Skip if this is not a JSON string (not an array subscription)
if (!key.startsWith('[') || !key.endsWith(']')) return;
try {
// Parse the array key
const properties = JSON.parse(key);
// Check if this property is in the subscription
if (properties.includes(prop)) {
this._notifySubscribers(fns, { [prop]: value }, calledCallbacks);
}
} catch (e) {
// Ignore parsing errors
}
});
}
_notifyMultiPropSubscribers(calledCallbacks) {
let _private = this[__.private];
// 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 && Object.keys(_private.pendingChanges).length > 0) {
let commonFns = this._findCommonValues(_private._subscribers, Object.keys(_private.pendingChanges));
if (commonFns.length > 0) {
this._notifySubscribers(commonFns, _private.pendingChanges, calledCallbacks);
}
}
}
_notifyPropertySubscriber(prop, value, calledCallbacks) {
let _private = this[__.private];
// Get the function by prop and trigger these functions.
let fns = _private._subscribers.get(prop);
if (fns && fns.length > 0) {
this._notifySubscribers(fns, { [prop]: value }, calledCallbacks);
}
}
/**
* 通知订阅者
* @param {Array} subscribers - 订阅者数组
* @param {object} data - 要发送的数据
* @param {Set} calledCallbacks - 已调用的回调函数集合
* @private
*/
_notifySubscribers(subscribers, data, calledCallbacks) {
subscribers.forEach(fn => {
// Skip if this callback has already been called
if (calledCallbacks.has(fn)) return;
// Mark this callback as called
calledCallbacks.add(fn);
fn({ data });
});
}
}
export { MonitorDataComponent }