@citrineos/util
Version:
The OCPP util module which supplies helpful utilities like cache and queue connectors, etc.
170 lines • 6.87 kB
JavaScript
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import { plainToInstance } from 'class-transformer';
/**
* Implementation of cache interface with memory storage
*/
export class MemoryCache {
_cache;
_keySubscriptionMap;
_keySubscriptionPromiseMap;
_timeoutMap;
constructor() {
const keySubscriptionMap = new Map();
const subscriptionHandler = {
// Returns value on keySubscriptions when Map.set(key, value) is called
set(target, property, value) {
const setOutcome = Reflect.set(target, property, value);
if (typeof property === 'string' && keySubscriptionMap.has(property) && setOutcome) {
(keySubscriptionMap?.get(property))(value);
}
return setOutcome;
},
// Returns null on keySubscriptions when Map.delete(key) is called
deleteProperty(target, property) {
const deleteOutcome = Reflect.deleteProperty(target, property);
if (typeof property === 'string' && keySubscriptionMap.has(property) && deleteOutcome) {
(keySubscriptionMap?.get(property))(null);
}
return deleteOutcome;
},
// Here to support Map.get and Map.has, does not alter behavior
get(target, property) {
const value = Reflect.get(target, property);
if (typeof value === 'function') {
// Return a bound version of the method to the original target
return value.bind(target);
}
return value;
},
};
this._cache = new Proxy(new Map(), subscriptionHandler);
this._keySubscriptionMap = keySubscriptionMap;
this._keySubscriptionPromiseMap = new Map();
this._timeoutMap = new Map();
}
exists(key, namespace) {
namespace = namespace || 'default';
const namespaceKey = `${namespace}:${key}`;
return Promise.resolve(this._cache.has(namespaceKey));
}
existsAnyInNamespace(namespace) {
const prefix = `${namespace}:`;
for (const key of this._cache.keys()) {
if (key.startsWith(prefix)) {
return Promise.resolve(true);
}
}
return Promise.resolve(false);
}
async remove(key, namespace) {
namespace = namespace || 'default';
const namespaceKey = `${namespace}:${key}`;
return this._cache.delete(namespaceKey);
}
onChange(key, waitSeconds, namespace, classConstructor) {
namespace = namespace || 'default';
const namespaceKey = `${namespace}:${key}`;
// Either get existing promise awaiting change on this key or create a new one and store it.
// This way, any number of threads can wait for the same key at the same time.
// Type must include 'undefined' due to Map.get(key)'s return type, however in no case can it actually be undefined.
const onChangeValuePromise = this._keySubscriptionPromiseMap.has(namespaceKey)
? this._keySubscriptionPromiseMap.get(namespaceKey)
: this._keySubscriptionPromiseMap
.set(namespaceKey, new Promise((resolve) => {
this._keySubscriptionMap.set(namespaceKey, (value) => {
resolve(value);
this._keySubscriptionMap.delete(namespaceKey);
this._keySubscriptionPromiseMap.delete(namespaceKey);
});
}))
.get(namespaceKey);
return Promise.race([
onChangeValuePromise?.then((value) => {
if (typeof value === 'string') {
if (classConstructor) {
return plainToInstance(classConstructor(), JSON.parse(value));
}
else {
return value;
}
}
else {
return value;
}
}),
new Promise((resolve) => {
setTimeout(() => {
resolve(this.get(key, namespace, classConstructor));
}, waitSeconds * 1000);
}),
]);
}
async get(key, namespace, classConstructor) {
namespace = namespace || 'default';
const namespaceKey = `${namespace}:${key}`;
const result = this._cache.get(namespaceKey);
if (result) {
if (classConstructor) {
return plainToInstance(classConstructor(), JSON.parse(result));
}
return result;
}
return null;
}
async set(key, value, namespace, expireSeconds) {
namespace = namespace || 'default';
const namespaceKey = `${namespace}:${key}`;
this._cache.set(namespaceKey, value);
if (this._timeoutMap.has(namespaceKey)) {
clearTimeout(this._timeoutMap.get(namespaceKey));
}
if (expireSeconds) {
this._timeoutMap.set(namespaceKey, setTimeout(() => {
this._cache.delete(namespaceKey);
}, expireSeconds * 1000));
}
this.resolveOnChange(namespaceKey, value);
return true;
}
async setIfNotExist(key, value, namespace, expireSeconds) {
namespace = namespace || 'default';
const namespaceKey = `${namespace}:${key}`;
if (this._cache.has(namespaceKey)) {
return false;
}
this._cache.set(namespaceKey, value);
if (this._timeoutMap.has(namespaceKey)) {
clearTimeout(this._timeoutMap.get(namespaceKey));
}
if (expireSeconds) {
this._timeoutMap.set(namespaceKey, setTimeout(() => {
this._cache.delete(namespaceKey);
}, expireSeconds * 1000));
}
this.resolveOnChange(namespaceKey, value);
return true;
}
async updateExpiration(key, expireSeconds, namespace) {
namespace = namespace || 'default';
const namespaceKey = `${namespace}:${key}`;
if (!this._cache.has(namespaceKey)) {
return false;
}
if (this._timeoutMap.has(namespaceKey)) {
clearTimeout(this._timeoutMap.get(namespaceKey));
}
this._timeoutMap.set(namespaceKey, setTimeout(() => {
this._cache.delete(namespaceKey);
}, expireSeconds * 1000));
return true;
}
resolveOnChange(namespaceKey, value) {
const resolveOnChangeCallback = this._keySubscriptionMap.get(namespaceKey);
if (resolveOnChangeCallback) {
resolveOnChangeCallback(value);
}
}
}
//# sourceMappingURL=memory.js.map