s7webserverapi
Version:
Unofficial Simatic-S7-Webserver JSON-RPC-API Client for S7-1200/1500 PLCs
238 lines (237 loc) • 8.96 kB
JavaScript
;
/* eslint-disable @typescript-eslint/no-explicit-any */
Object.defineProperty(exports, "__esModule", { value: true });
exports.CacheStructure = void 0;
/**
* Helper class that holds the cache-structure for the PLC-Connector.
* Its used to easily access and write values in the cache given a flattened key that fits to the template T
*
* @export
* @class CacheStructure
* @typedef {CacheStructure}
* @template T - The type of the cacheObject that is used to store the values
*/
class CacheStructure {
/**
* Holds the actual values of the cache
*
* @public
* @type {{}}
*/
cacheObject = {};
/**
* Parses a flattened key into an array of key-elements. A number is later interpreted as an array-index. A string is interpreted as an object-key
*
* @public
* @param {(FlattenKeys<T> | string)} flattenedKey
* @returns {(string | number)[]}
*/
parseFlattenedKey(flattenedKey) {
return flattenedKey.split('.').map(key => {
// Check if the key is a number and convert it if so
return isNaN(Number(key)) ? key : Number(key);
});
}
constructor(initObject) {
this.cacheObject = initObject ?? {};
}
/**
* Static helper function that gets the next reference from a given reference and a new key.
*
* @public
* @static
* @param {*} ref
* @param {string} newKey
* @returns {any}
*/
static getNextReference(ref, newKey) {
if (newKey == '' || ref == undefined) {
return ref;
}
if (!Number.isNaN(Number(newKey))) {
if (Array.isArray(ref)) {
ref = ref[Number(newKey)];
return ref;
}
else {
throw new Error(`Error in getting next reference: ${newKey} is not a valid key for the given reference, ${newKey} is a number, but the given reference is not an array`);
}
}
if (typeof ref === 'object' && !Array.isArray(ref)) {
if (!(newKey in ref)) {
throw new Error(`Error in getting next reference: ${newKey} is not a valid key for the given reference`);
}
ref = ref[newKey];
return ref;
}
throw new Error(`Error in getting next reference: ${newKey} is not a valid key for the given ref, ${newKey} is a string, but the given reference is not an object`);
}
/**
* Checks if an entry with the given flattened key exists in the cache
*
* @public
* @param {FlattenKeys<T>} flattenedKey
* @returns {boolean}
*/
entryExists(flattenedKey) {
const keys = this.parseFlattenedKey(flattenedKey);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let current = this.cacheObject;
for (const key of keys) {
if (current == null || !(key in current)) {
return false;
}
if (Array.isArray(current) && typeof key === 'number') {
current = current[key];
}
else if (typeof key === 'string' && typeof current === 'object' && !Array.isArray(current)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
current = current[key];
}
}
return true;
}
/**
* Gets a copy of a value in the cache. Useful if the value is an object and we need to change the value somewhere else without chaning the original value in the cache
*
* @public
* @param {FlattenKeys<T>} flattenedKey
* @returns {*}
*/
getCopy(flattenedKey) {
const ref = this.getReference(flattenedKey);
if (ref == undefined) {
return undefined;
}
return this.deepClone(ref);
}
/**
* Recursive function to deeply clone an object. Since we only have a simple structure that is deeply nested this function should be enough.
* @param obj Object to clone
* @returns
*/
deepClone(obj) {
if (obj == undefined || typeof obj !== 'object') {
return obj;
}
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
clone[key] = this.deepClone(obj[key]);
}
}
return clone;
}
/**
* Gets a reference to a value in the cache. Useful if the value is an object and we need to change the value somewhere else and want to change the original value in the cache
*
* @public
* @param {FlattenKeys<T>} flattenedKey
* @returns {any}
*/
getReference(flattenedKey) {
const keys = this.parseFlattenedKey(flattenedKey);
let current = this.cacheObject;
for (const key of keys) {
if (Array.isArray(current) && typeof key === 'number') {
current = current[key];
}
else if (typeof key === 'string' && typeof current === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
current = current[key];
}
else {
return undefined;
}
}
return current;
}
/**
* Writes a value to the cache given a flattened key. It also builds up the structure if it does not exist yet.
* E.g. if the cache is completley empty {} and we call this function with ("test.someArray", [0,1,2]) the resulting cache looks liek this at the end:
* {
* test: {
* someArray: [0,1,2]
* }
* }
*
* @public
* @param {FlattenKeys<T>} flattenedKey
* @param {*} value
*/
writeEntry(flattenedKey, value) {
const keys = this.parseFlattenedKey(flattenedKey);
let current = this.checkAndFillCacheStructure(flattenedKey);
if (Array.isArray(current) && typeof keys[keys.length - 1] === 'number') {
current[keys[keys.length - 1]] = value;
}
else if (typeof keys[keys.length - 1] === 'string' && typeof current === 'object') {
current = current;
current[keys[keys.length - 1]] = value;
}
}
/**
* Checks if the key is correct and fills up the cache structure up to the second last key. this is used before we write a value to the cache.
* @param flattenedKey
* @returns the reference to the second last key in the cache structure
*/
checkAndFillCacheStructure(flattenedKey) {
const keys = this.parseFlattenedKey(flattenedKey);
let current = this.cacheObject;
keys.slice(0, -1).forEach((key, index) => {
current = this.ensureKeyExists(current, key, keys[index + 1]);
});
return current;
}
/**
*
* @param current Reference to the current level in the cache structure
* @param key The key that should be checked, relative to the current level
* @param nextKey The next key that should be checked. Given this key we can determine if we need to insert an array or an object
* @returns the reference to the next level in the cache structure, given the key.
*/
ensureKeyExists(current, key, nextKey) {
if (!(key in current)) {
current[key] = this.initializeNextLevel(current, key, nextKey);
}
return current[key];
}
/**
*
* @param current Reference to the current level in the cache structure
* @param key The key that should be initialized
* @param nextKey The next key that should be initialized. Given this next key, we can determine if we need to insert an array or an object
* @returns the initialized next level in the cache structure
*/
initializeNextLevel(current, key, nextKey) {
const returnValue = this.determineNewLevelType(nextKey);
if (Array.isArray(current) && typeof key === 'number') {
return returnValue;
}
if (typeof key === 'string' && typeof current === 'object') {
return returnValue;
}
throw new Error('Error while filling in the cache structure, key is not a valid key for current object. Key: ' + key + ', Current: ' + current);
}
/**
* Determines if the next level in the cache structure should be an array or an object
* Placeholder if in the future we want to use a different structure for the cache
* @param nextKey
* @returns
*/
determineNewLevelType(nextKey) {
return typeof nextKey === 'number' ? [] : {};
}
hmiKeyLoaded(key) {
if (Array.isArray(key)) {
for (const singleKey of key) {
if (!this.entryExists(singleKey)) {
return false;
}
}
return true;
}
return this.entryExists(key);
}
}
exports.CacheStructure = CacheStructure;