Source: service/storage/StorageService.js

(function() {
    /**
     * @typedef {Object} Help4.service.StorageService.Params
     */

    /**
     * @typedef {Object} Help4.service.StorageService.IterationParams
     * @property {string[]|string} [target = null] - [<storageName>, <storageName>, ...] or <storageName>
     * @property {string[]|string} [exclude = null] - [<storageName>, <storageName>, ...] or <storageName>
     */

    /**
     * @typedef {Function} Help4.service.StorageService.IterationExecutor
     * @param {Help4.service.StorageService.StorageInterface} storage - the current storage interface
     * @returns {Promise<boolean>} - true: stop execution
     */

    /**
     * Usage: call {@link Help4.service.StorageService#isStorageAvailable} or {@link Help4.service.StorageService#prepareAll} before using {@link Help4.service.StorageService#get}, {@link Help4.service.StorageService#set} or {@link Help4.service.StorageService#del}
     * @augments Help4.jscore.Base
     * @property {Object} _config
     * @property {Object} _storages
     * @property {Object} _storageIds
     * @property {Object} STORAGE_ID
     */
    Help4.service.StorageService = class extends Help4.jscore.Base {
        /**
         * @override
         * @constructor
         */
        constructor() {
            super({
                statics: {
                    _config:     {init: {}, destroy: false},
                    _storages:   {init: {}, destroy: false},
                    _storageIds: {init: {}, destroy: false},
                    STORAGE_ID:  {init: {}, destroy: false}
                }
            });

            _init.call(this);

            const {STORAGE_ID, _storages} = this;
            const {STORAGE_ID: STATIC_STORAGE_ID} = this.constructor;

            (this._storageIds = Object.keys(_storages)).forEach(id => {
                STORAGE_ID[id] = id;
                STATIC_STORAGE_ID[id] = id;
            });
        }

        static STORAGE_LEVEL = {
            manual: -1,     // will never be picked automatically, e.g. back ends that require specific authentication
            cookie: 0,      // store in Cookies - worst
            local: 1,       // store in LocalStore or SessionStore - 2nd best
            application: 2  // store in application - best
        }

        static STORAGE_ID = {}

        /** @override */
        destroy() {
            for (const storage of Object.values(this._storages)) {
                storage.destroy();
            }

            delete this._config;
            delete this._storages;
            delete this._storageIds;
            delete this.STORAGE_ID;

            super.destroy();
        }

        /** @param {Object} config */
        config(config) {
            this._config = config;
        }

        /**
         * @param {string[]} [exclude = []]
         * @returns {Promise<void>}
         */
        async prepareAll(exclude = []) {
            exclude = new Set(exclude);

            const promises = [];
            for (const storageId of this._storageIds) {
                if (exclude.has(storageId)) continue;

                const promise = this.isStorageAvailable(storageId);
                promises.push(promise);
            }
            await Help4.Promise.all(promises);
        }

        /**
         * @param {string} storageId
         * @returns {Promise<boolean>}
         */
        async isStorageAvailable(storageId) {
            const {_storages, _config} = this;

            const storage = _storages[storageId];
            if (!storage) return false;

            const config = _config[storageId];
            if (config) {
                // storage interface has a config that is not yet used
                // interface will only return once configured and ready
                delete _config[storageId];  // just configure once
                await storage.config(config);
            } else {
                // wait, that the storage completes its initialization
                await storage.waitReady();
            }

            return storage.available;
        }

        /**
         * @param {string} storageId
         * @returns {Promise<?Help4.service.StorageService.StorageInterface>}
         */
        async getStorage(storageId) {
            const available = await this.isStorageAvailable(storageId);
            return available ? this._storages[storageId] : null;
        }

        /**
         * @param {string} key
         * @param {Help4.service.StorageService.IterationParams} [params]
         * @returns {Promise<*>}
         */
        async get(key, params) {
            // get from the 1st available storage
            let result = undefined;
            await _iterate.call(this, async storage => !!(result = await storage.get(key)), params);
            return result;
        }

        /**
         * @param {string} key
         * @param {*} value
         * @param {Help4.service.StorageService.IterationParams} [params]
         * @returns {Promise<boolean>}
         */
        async set(key, value, params) {
            // set to the 1st available storage
            let success = false;
            await _iterate.call(this, async storage => success = await storage.set(key, value), params);
            return success;
        }

        /**
         * @param {string} key
         * @param {Help4.service.StorageService.IterationParams} [params]
         * @returns {Promise<void>}
         */
        async del(key, params) {
            // delete from ALL available storages
            return _iterate.call(this, storage => storage.del(key) && false, params);
        }
    }

    /**
     * @memberof Help4.service.StorageService#
     * @private
     */
    function _init() {
        const {StorageService} = Help4.service;
        const {_storages} = this;

        for (const [key, storageInterface] of Object.entries(StorageService)) {
            if (typeof storageInterface === 'function' && key.match(/Storage$/)) {
                const name = key[0].toLowerCase() + key.substring(1).replace(/Storage$/, '');  // EnableNowStorage => enableNow
                _storages[name] = new StorageService[key]();
            }
        }
    }

    /**
     * @memberof Help4.service.StorageService#
     * @private
     * @param {Help4.service.StorageService.IterationExecutor} executor
     * @param {Help4.service.StorageService.IterationParams} [params = {}]
     * @returns {Promise<void>}
     */
    async function _iterate(executor, {target = null, exclude = null} = {}) {
        const {STORAGE_LEVEL: {manual}} = this.constructor;
        const {_config, _storages} = this;

        const requestedTargetIds = new Set(typeof target === 'string' ? [target] : (target || []));
        const excludeIds = new Set(typeof exclude === 'string' ? [exclude] : (exclude || []));
        const targetIds = new Set(requestedTargetIds.size ? requestedTargetIds : this._storageIds);

        const promises = /** @type {Array<Promise<void>>} */ [];
        targetIds.forEach(storageId => {
            // filter:
            // - not excluded
            // - interface exists
            // - not manual or explicitly requested
            if (!excludeIds.has(storageId)) {
                const storage = _storages[storageId];
                if (storage && (storage.storageLevel !== manual || requestedTargetIds.has(storageId))) {
                    // storage accepted; configure if needed
                    if (_config[storageId]) {
                        const promise = _storages[storageId].config(_config[storageId]);
                        delete _config[storageId];
                        promises.push(promise);
                    }
                    return;
                }
            }

            // storage not accepted
            targetIds.delete(storageId);
        });

        await Help4.Promise.all(promises);
        if (this.isDestroyed()) return;

        const sortedTargetIds = /** @type {string[]} */ [...targetIds]
        .filter(storageId => _storages[storageId].available)  // filter: storage available in current system?
        .sort((storageId1, storageId2) => _storages[storageId2].storageLevel - _storages[storageId1].storageLevel);  // sort by storageLevel

        // execute until
        // - no more storages left
        // - one storage executed successfully ("stop" condition)
        let index = 0;
        const exec = async () => {
            if (!this.isDestroyed() && index < sortedTargetIds.length) {
                const storage = _storages[sortedTargetIds[index++]];
                const stop = await executor(storage);
                if (!stop) return exec();
            }
        };
        await exec();
    }
})();