Skip to content

Commit

Permalink
added filtering to state generator, updated filters readme to fix err…
Browse files Browse the repository at this point in the history
…or, more robust hash function
  • Loading branch information
jvigliotta committed Nov 19, 2024
1 parent a6ed866 commit 9eacc84
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 28 deletions.
13 changes: 10 additions & 3 deletions example/generator/GeneratorMetadataProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,16 @@ const METADATA_BY_TYPE = {
string: 'ON'
}
],
hints: {
range: 1
}
filters: [
{
singleSelectionThreshold: true,
comparator: 'equals',
possibleValues: [
{ label: 'OFF', value: 0 },
{ label: 'ON', value: 1 }
]
}
]
},
{
key: 'value',
Expand Down
22 changes: 11 additions & 11 deletions example/generator/GeneratorProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
GeneratorProvider.prototype.supportsRequest = GeneratorProvider.prototype.supportsSubscribe =
GeneratorProvider.prototype.canProvideTelemetry;

GeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request) {
GeneratorProvider.prototype.makeWorkerRequest = function (domainObject, options) {
var props = [
'amplitude',
'period',
Expand All @@ -59,7 +59,7 @@ GeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request)
'exceedFloat32'
];

request = request || {};
options = options || {};

var workerRequest = {};

Expand All @@ -71,8 +71,8 @@ GeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request)
workerRequest[prop] = domainObject.telemetry[prop];
}

if (request && Object.prototype.hasOwnProperty.call(request, prop)) {
workerRequest[prop] = request[prop];
if (options && Object.prototype.hasOwnProperty.call(options, prop)) {
workerRequest[prop] = options[prop];
}

if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) {
Expand All @@ -88,17 +88,17 @@ GeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request)
return workerRequest;
};

GeneratorProvider.prototype.request = function (domainObject, request) {
var workerRequest = this.makeWorkerRequest(domainObject, request);
workerRequest.start = request.start;
workerRequest.end = request.end;
workerRequest.size = request.size;
workerRequest.strategy = request.strategy;
GeneratorProvider.prototype.request = function (domainObject, options) {
var workerRequest = this.makeWorkerRequest(domainObject, options);
workerRequest.start = options.start;
workerRequest.end = options.end;
workerRequest.size = options.size;
workerRequest.strategy = options.strategy;

return this.workerInterface.request(workerRequest);
};

GeneratorProvider.prototype.subscribe = function (domainObject, callback) {
GeneratorProvider.prototype.subscribe = function (domainObject, callback, options) {
var workerRequest = this.makeWorkerRequest(domainObject, {});

return this.workerInterface.subscribe(workerRequest, callback);
Expand Down
28 changes: 23 additions & 5 deletions example/generator/StateGeneratorProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@ StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'example.state-generator';
};

StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback, options) {
var duration = domainObject.telemetry.duration * 1000;

var interval = setInterval(function () {
var interval = setInterval(() => {
var now = Date.now();
var datum = pointForTimestamp(now, duration, domainObject.name);
datum.value = String(datum.value);
callback(datum);
if (!this.shouldBeFiltered(datum, options)) {
datum.value = String(datum.value);
callback(datum);
}
}, duration);

return function () {
Expand All @@ -63,9 +65,25 @@ StateGeneratorProvider.prototype.request = function (domainObject, options) {

var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, duration, domainObject.name));
const point = pointForTimestamp(start, duration, domainObject.name);

if (!this.shouldBeFiltered(point, options)) {
data.push(point);
}
start += duration;
}

return Promise.resolve(data);
};

StateGeneratorProvider.prototype.shouldBeFiltered = function (point, options) {
const valueToFilter = options?.filters?.state?.equals?.[0];

if (!valueToFilter) {
return false;
}

const { value } = point;

return value !== Number(valueToFilter);
};
57 changes: 51 additions & 6 deletions src/api/telemetry/TelemetryAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,28 +278,73 @@ export default class TelemetryAPI {
}, {});
}

/**
* Filters out class instances and functions from an object, returning a plain
* data object suitable for JSON serialization.
*
* @private
* @param {Object|Array|*} value The value to sanitize
* @returns {Object|Array|*} A sanitized copy with class instances and functions removed
*/
#sanitizeForSerialization(value) {
if (value === null || value === undefined) {
return value;
}

// Handle primitive types, arrays and objects
if (typeof value !== 'object') {
return typeof value !== 'function' ? value : undefined;
}

if (Array.isArray(value)) {
return value.map((item) => this.#sanitizeForSerialization(item));
}

if (Object.getPrototypeOf(value) === Object.prototype) {
const result = {};

for (const key of Object.keys(value)) {
const sanitizedValue = this.#sanitizeForSerialization(value[key]);

if (sanitizedValue !== undefined) {
result[key] = sanitizedValue;
}
}
return result;
}

return undefined;
}

/**
* Generates a numeric hash value for an options object. The hash is consistent
* for equivalent option objects regardless of property order.
*
* This is used to create compact, unique cache keys for telemetry subscriptions with
* different options configurations. The hash function ensures that identical options
* objects will always generate the same hash value.
* objects will always generate the same hash value, while different options objects
* (even with small differences) will generate different hash values.
*
* @private
* @param {Object} options The options object to hash
* @returns {number} A 32-bit integer hash of the options object
* @returns {number} A positive integer hash of the options object
*/
#hashOptions(options) {
let hash = 0;
const canonicalOptions = this.#canonicalizeOptions(options);
const sanitizedOptions = this.#sanitizeForSerialization(options);
const canonicalOptions = this.#canonicalizeOptions(sanitizedOptions);
const str = JSON.stringify(canonicalOptions);

let hash = 0;
const prime = 31;
const modulus = 1e9 + 9; // Large prime number

for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = hash * 31 + char;
// Calculate new hash value while keeping numbers manageable
hash = Math.floor((hash * prime + char) % modulus);
}
return hash;

return Math.abs(hash);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/plugins/filters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ To define a filter, you'll need to add a new `filter` property to the domain obj
singleSelectionThreshold: true,
comparator: 'equals',
possibleValues: [
{ name: 'Apple', value: 'apple' },
{ name: 'Banana', value: 'banana' },
{ name: 'Orange', value: 'orange' }
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Orange', value: 'orange' }
]
}]
}
Expand Down

0 comments on commit 9eacc84

Please sign in to comment.