Files
openmct/example/exampleStalenessProvider/ExampleStalenessProvider.js
David Tsay acccc3a2c3 Respect latest available staleness prior to time conductor start (#8211)
* add clearStaleness method since === SKIP_CHECK flag

* fix logic for updating staleness
* should be inclusive to start and end bounds
* should respect prior to start bounds if stale
* should not show staleness for after end bounds

* add `ExampleStalenessProvider`
update telemetry api jsdocs for staless provider

* move sine wave staleness tests into appropriate folder location

* convert `StateGenerator` into class

* clean up coding style

* use timesystem key and now() from openmct time api

* fix `ExampleStalenessProvider` initial conditions
install `ExampleStalenessProvider` by default in index.html

* Revert "fix logic for updating staleness"
To allow contribution from marcelo-earth

This reverts commit 3baef169f7.

* Refactor shouldUpdateStaleness to accept updates based on timestamp

* clarify comment
remove unused code

* fix import paths

* clean up staleness provider
write e2e test for staleness

* fix 404s to example imagery breaking e2e tests

---------

Co-authored-by: Marcelo Arias <hello@marceloarias.com>
2026-01-05 13:43:58 -08:00

137 lines
5.0 KiB
JavaScript

/*****************************************************************************
* Open MCT, Copyright (c) 2014-2025, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* @implements {import('src/api/telemetry/TelemetryAPI').StalenessProvider}
*/
export default class ExampleStalenessProvider {
#intervalId;
constructor(openmct, config = { stalenessInterval: 3000, reportStalenessInterval: 300 }) {
this.openmct = openmct;
this.stalenessInterval = config.stalenessInterval;
this.reportStalenessInterval = config.reportStalenessInterval;
this.observingStaleness = {};
this.latestReceivedTelemetry = {};
this.#observeTimeSystem();
this.#observeStaleness();
}
#observeTimeSystem() {
this.openmct.time.on('timeSystemChanged', () => {
this.timeSystem = this.openmct.time.getTimeSystem();
});
}
supportsStaleness(domainObject) {
return this.openmct.telemetry.isTelemetryObject(domainObject);
}
subscribeToStaleness(domainObject, callback) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.observingStaleness[keyString] = { callback };
const unsubscribe = this.openmct.telemetry.subscribe(domainObject, (datum) => {
this.#updateLatestReceivedTelemetry(domainObject, datum);
});
return () => {
delete this.observingStaleness[keyString];
unsubscribe?.();
if (Object.keys(this.observingStaleness).length === 0) {
clearInterval(this.#intervalId);
}
};
}
#observeStaleness() {
this.#intervalId = setInterval(() => {
if (!this.timeSystem) {
return;
}
Object.entries(this.observingStaleness).forEach(([keyString, observer]) => {
if (!this.latestReceivedTelemetry[keyString]) {
return;
}
const now = this.openmct.time.now();
const isStale = now - this.latestReceivedTelemetry[keyString] >= this.stalenessInterval;
// Overly reports when not stale because of generated telemetry flake
if (!isStale || !observer.response || isStale !== observer.response.isStale) {
const stalenessResponseObject = {
isStale,
[this.timeSystem.key]: now
};
observer.response = stalenessResponseObject;
observer.callback(stalenessResponseObject);
}
});
}, this.reportStalenessInterval);
}
/**
* @param {*} domainObject
* @returns {import('src/api/telemetry/TelemetryAPI').StalenessResponseObject}
*/
async isStale(domainObject) {
if (!this.timeSystem) {
this.timeSystem = this.openmct.time.getTimeSystem();
}
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.latestReceivedTelemetry[keyString]) {
// Naively assumes sorted request response so uses last datum in array
const response = await this.openmct.telemetry.request(domainObject, { strategy: 'latest' });
const lastDatum = response?.length ? response[response.length - 1] : undefined;
this.#updateLatestReceivedTelemetry(domainObject, lastDatum);
}
const timestamp = this.latestReceivedTelemetry[keyString];
if (timestamp) {
const isStale = this.openmct.time.now() - timestamp >= this.stalenessInterval;
const stalenessResponseObject = { isStale };
stalenessResponseObject[this.timeSystem.key] = timestamp;
return stalenessResponseObject;
}
}
#updateLatestReceivedTelemetry(domainObject, datum) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const metadata = this.openmct.telemetry.getMetadata(domainObject);
const metadataValue = metadata.value(this.timeSystem.key) || { format: this.timeSystem.key };
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
const timestamp = valueFormatter.parse(datum);
if (timestamp) {
this.latestReceivedTelemetry[keyString] = timestamp;
} else {
console.warn('Could not parse timestamp for staleness check');
}
}
}