node-red-contrib-xstate-machine
Version:
Xstate-based state machine implementation using state-machine-cat visualization for node red.
168 lines (133 loc) • 4.9 kB
JavaScript
const fs = require('fs')
const util = require('util');
const crypto = require('crypto');
const path = require('path');
const clone = require('clone');
async function getCache(RED) {
let settings = RED.settings.get('smxstate');
if( settings && settings.hasOwnProperty('cache') && Array.isArray(settings.cache) ) {
return settings.cache;
} else {
await setCache(RED,[]);
return [];
}
}
async function setCache(RED, cache) {
let settings = RED.settings.get('smxstate') || {};
Object.assign( settings, { cache: clone(cache) } )
await RED.settings.set('smxstate', settings);
}
function getCacheEntryIdxFromHash(cache, hash) {
return cache.findIndex( (el) => el.hash === hash);
}
function getCacheEntryIdx(cache, input) {
// Get hash for input
let hash = getInputHash(input);
// Find in cache
return getCacheEntryIdxFromHash(cache, hash);
}
function getCacheEntry(cache, input) {
// Try to get from cache
let cacheHitIdx = getCacheEntryIdx(cache, input);
return cache[cacheHitIdx];
}
function getCacheEntryFromHash(cache, hash) {
// Try to get from cache
let cacheHitIdx = getCacheEntryIdxFromHash(cache, hash);
return cache[cacheHitIdx];
}
function removeCacheEntry(cache, entry) {
if( entry.hasOwnProperty('hash') ) {
let idx = getCacheEntryIdxFromHash(cache, entry.hash);
if( idx >= 0 && idx < cache.length )
cache.splice(idx, 1);
}
return cache;
}
function addCacheEntry(cache, hash, file, ttl) {
// Write new cache entry
cache.push({
expire: Date.now() + ttl*1000,
file: file,
hash: hash
});
return cache;
}
function getInputHash(input) {
return crypto.createHash('md5').update(input).digest('hex');
}
async function ensureDirectory(directory) {
try {
await fs.promises.stat(directory);
} catch( err ) {
await fs.promises.mkdir(directory, { recursive: true });
}
}
async function removeCachedFile(RED, cacheEntry) {
try {
let cacheFile = path.join(RED.settings.userDir, 'smxstate', cacheEntry.file);
await fs.promises.stat(cacheFile);
await fs.promises.unlink(cacheFile);
} catch(err) {
// Do nothing
}
}
async function cacheRendering(RED, smcatStr, data, ttl) {
let cache = await getCache(RED);
let hash = getInputHash(smcatStr);
let cacheHit = getCacheEntryFromHash(cache, hash);
if( cacheHit ) {
// Remove
await setCache(RED, removeCacheEntry(cache, cacheHit));
// Remove cached file if it exists
await removeCachedFile(RED, cacheHit);
}
let file = `${hash}.json`;
let filedir = path.join(RED.settings.userDir, 'smxstate');
let cacheFile = path.join(filedir, file);
try {
// Ensure the target dir exists
await ensureDirectory(filedir);
// Write cached file
await fs.promises.writeFile(cacheFile, JSON.stringify(data));
} catch(err) {
console.error(`Error writing smxstate cache file ${cacheFile}: ${err}`);
return false;
}
// Update cache
await setCache(RED, addCacheEntry(cache, hash, file, ttl) );
return true;
}
async function getCachedRendering(RED, smcatStr) {
// Try to get from cache
let cache = await getCache(RED);
let cacheHit = getCacheEntry(cache, smcatStr);
if( cacheHit ) {
// Check if the cached file exists
try {
let cacheFile = '';
if( cacheHit.hasOwnProperty('file') ) {
cacheFile = path.join( RED.settings.userDir, 'smxstate', cacheHit.file );
if( !await util.promisify(fs.exists)(cacheFile) ) {
throw('cache file missing');
}
} else throw('file property missing');
if( cacheHit.hasOwnProperty('expire') && Date.now() < cacheHit.expire ) {
// Read from file and return
let content = JSON.parse(await fs.promises.readFile(cacheFile, 'utf8'));
if( content ) return content;
else throw(`invalid cache entry data in file ${cacheFile}`);
} else throw('cache entry is expired');
} catch(err) {
// Invalid cache entry, remove from cache
console.log(`Invalid smxstate cache entry: ${err}. Deleting from cache...`);
await setCache(RED, removeCacheEntry(cache, cacheHit));
// Remove cached file if it exists
await removeCachedFile(RED, cacheHit);
}
}
return null;
}
module.exports = {
getCachedRendering, cacheRendering
}