homebridge-homeconnect
Version:
A Homebridge plugin that connects Home Connect appliances to Apple HomeKit
121 lines • 4.28 kB
JavaScript
// Homebridge plugin for Home Connect home appliances
// Copyright © 2019-2025 Alexander Thoukydides
import { setImmediate as setImmediateP } from 'timers/promises';
import semver from 'semver';
import { MS, formatList, formatMilliseconds } from './utils.js';
import { logError } from './log-error.js';
import { PLUGIN_VERSION } from './settings.js';
// A simple persistent cache, with soft expiry
export class PersistCache {
log;
persist;
preferred;
// Key used to store/retrieve the cache from persistent storage
cacheName;
// Wait for cache to be read from persistent storage
initialised;
// Saving the cache to persistent storage
saving;
pendingSave;
// Local copy of the cache
cache = new Map();
// Length of time before values in the cache expire
ttl = 24 * 60 * 60 * MS; // (24 hours in milliseconds)
// Initialise a cache
constructor(log, persist, name, preferred) {
this.log = log;
this.persist = persist;
this.preferred = preferred;
this.cacheName = `${name} cache`;
this.initialised = this.load();
}
// Retrieve an item from the cache
async get(key) {
await this.initialised;
const item = this.cache.get(key);
if (!item)
return;
return item.value;
}
// Retrieve an item, if it exists checking that is has not expired
async getWithExpiry(key) {
await this.initialised;
const item = this.cache.get(key);
let description = `"${key}"`;
const expired = [];
if (!item) {
expired.push('does not exist in cache');
}
else {
const age = Date.now() - item.updated;
description += ` [${item.preferred}, v${item.version ?? '?'}, updated ${formatMilliseconds(age)} ago]`;
if (!semver.satisfies(PLUGIN_VERSION, `^${item.version}`))
expired.push(`not written by v${PLUGIN_VERSION}`);
if (item.preferred !== this.preferred)
expired.push(`does not match preference ${this.preferred}`);
if (this.ttl < age)
expired.push('is too old');
}
this.log.debug(expired.length ? `Expired cache ${description} ${formatList(expired)}` : `Cache ${description}`);
return item && { value: item.value, valid: !expired.length };
}
// Write an item to the cache
async set(key, value) {
await this.initialised;
this.cache.set(key, {
value: value,
preferred: this.preferred,
updated: Date.now(),
version: PLUGIN_VERSION
});
this.log.debug(`"${key}" written to cache`);
await this.save();
}
// Load the cache
async load() {
try {
const cache = await this.persist.getItem(this.cacheName);
if (cache) {
this.cache = new Map(Object.entries(cache));
const entries = Object.keys(this.cache).length;
this.log.debug(`Cache loaded (${entries} entries)`);
}
else {
this.log.debug('No cache found');
}
}
catch (err) {
logError(this.log, 'Cache read', err);
}
}
// Schedule saving the cache
save() {
if (!this.pendingSave) {
const doSave = async () => {
if (this.saving)
await this.saving;
await setImmediateP();
this.saving = this.pendingSave;
this.pendingSave = undefined;
await this.saveDeferred();
this.saving = undefined;
};
this.pendingSave = doSave();
}
return this.pendingSave;
}
// Save the cache
async saveDeferred() {
try {
await this.initialised;
const cache = Object.fromEntries(this.cache);
await this.persist.setItem(this.cacheName, cache);
const entries = Object.keys(this.cache).length;
this.log.debug(`Cache saved (${entries} entries)`);
}
catch (err) {
logError(this.log, 'Cache write', err);
}
}
}
//# sourceMappingURL=persist-cache.js.map