services_cache.js

/**
 * @file services/cache.js
 * @copyright @spmhome @_2025
 * @author Scott Meesseman @spmeesseman
 *//** */

const { dirname } = require("path");
const { writeFile } = require("fs/promises");
const { createDirSync, existsSync, resolvePath, writeFileSync, readFileSync } = require("@spmhome/cmn-utils");
const { merge, clone, safeStringify, cryptoUtils, safeParse, create: createObj } = require("@spmhome/type-utils");

/**
 * @class WpwCache
 * @implements {IDisposable}
 */
class WpwCache
{
    /**
     * @type {WpwCrypto}
     */
    _crypto;
    /**
     * @private
     * @type {Record<string, any>}
     */
    _store;
    /**
     * @private
     * @type {string}
     */
    _file;
    /**
     * @type {WpwLogger}
     */
    _logger;


    /**
     * @param {WpwCacheOptions} options
     */
    constructor(options)
    {
        this._logger = options.logger;
        this._crypto = merge({ type: "none" }, options.crypto);
        const fileName = options.slug.replace(/\.json$/, "").replace(/^\./, "");
        this._file = resolvePath(options.dir, `.${fileName}`);
        this._store = this.read();
    }


    /**
     * @returns {Record<string, any>}
     */
    get data() { return this._store; }


    dispose() { /* this.saveAsync(); */ }

    /**
     * @param {string} data
     * @returns {string}
     */
    decrypt(data)
    {
        if (this._crypto.type === "none") {
            return data;
        }
        if (this._crypto.strength === "256-bit") {
            return cryptoUtils.decryptAes256(this._crypto.key, data);
        }
        return cryptoUtils.decryptAes128(this._crypto.key, data);
    }


    /**
     * @param {string} data
     * @returns {string}
     */
    encrypt(data)
    {
        if (this._crypto.type === "none") {
            return data;
        } if (this._crypto.strength === "256-bit") {
            return cryptoUtils.encryptAes256(this._crypto.key, data);
        } return cryptoUtils.encryptAes128(this._crypto.key, data);
    }

    /**
     * @template D
     * @overload
     * @param {string} item
     * @returns {D | undefined}
     */
    /**
     * @template D
     * @overload
     * @param {string} item
     * @param {D} defValue
     * @returns {D}
     */
    /**
     * @template D
     * @param {string} item
     * @param {D} [defValue]
     * @returns {D | undefined}
     */
    get(item, defValue) { return this._store[item] ? clone(this._store[item]) : defValue; }


    /**
     * @returns {Record<string, any>}
     */
    read()
    {
        if (!existsSync(dirname(this._file)))
        {
            createDirSync(dirname(this._file));
        }
        if (!existsSync(this._file))
        {
            writeFileSync(this._file, this.encrypt("{}"));
        }
        try {
            return safeParse(this.decrypt(readFileSync(this._file)));
        }
        catch (e)
        {   this._logger.error(e);
            writeFileSync(this._file, this.encrypt("{}"));
        }
        return createObj();
    };


    /**
     * @param {Record<string, any>} [store] The _store, as a JSON object
     */
    reset(store) { this._store = createObj(store); this.save(); };


    /**
     * @param {Record<string, any>} store The _store, as a JSON object
     * @returns {Promise<void>}
     */
    resetAsync(store) { this._store = createObj(store); return this.saveAsync(); };


    save() { writeFileSync(this._file, this.encrypt(safeStringify(this._store))); }


    /**
     * @returns {Promise<void>}
     */
    saveAsync() { return writeFile(this._file, this.encrypt(safeStringify(this._store))); }


    /**
     * @param {Record<string, any>} cacheObj The _store, as a JSON object
     */
    set(cacheObj) { merge(this._store, cacheObj); this.save(); };


    /**
     * @param {Record<string, any>} cacheObj The _store, as a JSON object
     * @returns {Promise<void>}
     */
    setAsync(cacheObj) { merge(this._store, cacheObj); return this.saveAsync(); };

}


module.exports = WpwCache;