UNPKG

@citrineos/util

Version:

The OCPP util module which supplies helpful utilities like cache and queue connectors, etc.

170 lines 6.87 kB
// 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