-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Telemetry API] Prevent Subscriptions with different options from overwriting each other #7930
base: master
Are you sure you want to change the base?
Changes from all commits
9f46d8a
cf406c9
0252bca
a6ed866
9eacc84
6ebc5bf
cea46ea
f994e67
65d901e
20538eb
172b902
6569c75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -250,6 +250,103 @@ export default class TelemetryAPI { | |
return options; | ||
} | ||
|
||
/** | ||
* Sanitizes objects for consistent serialization by: | ||
* 1. Converting non-plain objects (class instances) and functions to string markers (e.g. "@keyName") | ||
* 2. Sorting object keys alphabetically to ensure consistent ordering | ||
* 3. Recursively processing nested objects and arrays | ||
* | ||
* This is primarily used to generate consistent hash values for telemetry subscription options, | ||
* ensuring that equivalent options objects produce the same hash regardless of property order | ||
* or the presence of non-serializable values. While this would normally be a private method, | ||
* it is exposed for use in JSON.stringify. | ||
* | ||
* @param {string} key The current property key being processed | ||
* @param {Object|Array|*} value The value to sanitize | ||
* @returns {Object|Array|string|*} The sanitized value: | ||
* - Plain objects: A new object with sorted keys and sanitized values | ||
* - Arrays: A new array with sanitized elements | ||
* - Functions/Class instances: String in format "@keyName" | ||
* - Primitives: Returned as-is | ||
*/ | ||
sanitizeForSerialization(key, value) { | ||
// Handle null and primitives directly | ||
if (value === null || typeof value !== 'object') { | ||
return value; | ||
} | ||
|
||
// Handle arrays | ||
if (Array.isArray(value)) { | ||
return value.map((item, index) => this.sanitizeForSerialization(String(index), item)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just return the array itself, the replacer function (sanitizeForSerialization) will automatically be called for each member. see docs - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter |
||
} | ||
|
||
// Replace functions and non-plain objects with their key names | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Return |
||
if (typeof value === 'function' || Object.getPrototypeOf(value) !== Object.prototype) { | ||
return `@${key}`; | ||
} | ||
|
||
// Handle plain objects | ||
const sortedObject = {}; | ||
const keys = Object.keys(value).sort(); | ||
for (const objectKey of keys) { | ||
const itemValue = value[objectKey]; | ||
const sanitizedValue = this.sanitizeForSerialization(objectKey, itemValue); | ||
sortedObject[objectKey] = sanitizedValue; | ||
} | ||
|
||
return sortedObject; | ||
} | ||
|
||
/** | ||
* 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, 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 positive integer hash of the options object | ||
*/ | ||
#hashOptions(options) { | ||
const sanitizedOptionsString = JSON.stringify( | ||
options, | ||
this.sanitizeForSerialization.bind(this) | ||
); | ||
|
||
let hash = 0; | ||
const prime = 31; | ||
const modulus = 1e9 + 9; // Large prime number | ||
|
||
for (let i = 0; i < sanitizedOptionsString.length; i++) { | ||
const char = sanitizedOptionsString.charCodeAt(i); | ||
// Calculate new hash value while keeping numbers manageable | ||
hash = Math.floor((hash * prime + char) % modulus); | ||
} | ||
|
||
return Math.abs(hash); | ||
} | ||
|
||
/** | ||
* Generates a unique cache key for a telemetry subscription based on the | ||
* domain object identifier and options (which includes strategy). | ||
* | ||
* Uses a hash of the options object to create compact cache keys while still | ||
* ensuring unique keys for different subscription configurations. | ||
* | ||
* @private | ||
* @param {import('openmct').DomainObject} domainObject The domain object being subscribed to | ||
* @param {Object} options The subscription options object (including strategy) | ||
* @returns {string} A unique key string for caching the subscription | ||
*/ | ||
#getSubscriptionCacheKey(domainObject, options) { | ||
const keyString = makeKeyString(domainObject.identifier); | ||
|
||
return `${keyString}:${this.#hashOptions(options)}`; | ||
} | ||
|
||
/** | ||
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request | ||
* The request will be modified when it is received and will be returned in it's modified state | ||
|
@@ -418,16 +515,14 @@ export default class TelemetryAPI { | |
this.#subscribeCache = {}; | ||
} | ||
|
||
const keyString = makeKeyString(domainObject.identifier); | ||
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST; | ||
// Override the requested strategy with the strategy supported by the provider | ||
const optionsWithSupportedStrategy = { | ||
...options, | ||
strategy: supportedStrategy | ||
}; | ||
// If batching is supported, we need to cache a subscription for each strategy - | ||
// latest and batched. | ||
const cacheKey = `${keyString}:${supportedStrategy}`; | ||
|
||
const cacheKey = this.#getSubscriptionCacheKey(domainObject, optionsWithSupportedStrategy); | ||
let subscriber = this.#subscribeCache[cacheKey]; | ||
|
||
if (!subscriber) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Playwright gone done this already - https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-count