(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();
}
})();