(function() {
/**
* retrieve configuration; when to update data;
* see also {@link Help4.jscore.DataBase.RETRIEVE_KEYS}
* @typedef {Object} Help4.jscore.DataBase.Retrieve
* @property {boolean} onInitialize - retrieve data on initialize
* @property {boolean} onUpdate - retrieve data on update
* @property {boolean} onActivate - retrieve data on activate
*/
/**
* the actually managed data
* @typedef {Object} Help4.jscore.DataBase.Data
* @property {Help4.jscore.ControlBase.Types} type - data type
* @property {boolean} [cleanup = false] - whether to clean the data on class destroy
* @property {*} init - the init value
* @property {Help4.jscore.DataBase.Retrieve} retrieve - when to update the data
*/
/**
* additional class configuration
* @typedef {Object} Help4.jscore.DataBase.Config
* @property {Function} [onPropertyChange] - sync listener for property change events
*/
/**
* @typedef {Help4.jscore.ControlBase.Params} Help4.jscore.DataBase.Params
* @property {Object} [data] - data parameters; each is of type {@link Help4.jscore.DataBase.Data}
* @property {Object} [retrieve] - how to update the data: {<key>: async function getValueForKey}
* @property {Help4.jscore.DataBase.Config} [config] - class configuration
*/
/**
* to be used as a base class for data controls
* @augments Help4.jscore.ControlBase
* @abstract
*/
Help4.jscore.DataBase = class extends Help4.jscore.ControlBase {
/**
* @override
* @param {Object} [params]
* @param {Help4.jscore.DataBase.Params} [classConfig]
*/
constructor(params, classConfig) {
super(params, classConfig, true);
const {RETRIEVE_KEY} = this.constructor;
classConfig = this.____getClassConfig();
classConfig[RETRIEVE_KEY] = {};
this.____resolveDerivedParams(classConfig);
this.____defineProperties(params || {});
this.____defineStaticProperties();
this.____defineDataFunctions();
}
static DATA_KEYS = '_data_keys';
static RETRIEVE_KEY = '_retrieve';
/**
* @memberof Help4.jscore.DataBase
* @enum {string}
* @property {'initialize'} initialize
* @property {'update'} update
* @property {'activate'} activate
*/
static RETRIEVE_KEYS = {
initialize: 'initialize',
update: 'update',
activate: 'activate'
}
/** destroys this instance */
destroy() {
const classConfig = this.____getClassConfig();
for (const [key, info] of Object.entries(classConfig?.data || {})) {
const item = this[key]; // prevent multiple getter calls
if (item instanceof Help4.jscore.DataContainer || info.cleanup) item.destroy?.();
delete this[key];
}
super.destroy();
}
/** cleans all stored data */
clean() {
const {RETRIEVE_KEY} = this.constructor;
const rk = this.____getClassConfig()[RETRIEVE_KEY];
const ts = new Date().getTime();
// discard any upcoming result that has been started before clean()
// see _retrieve() for more information
for (const [id, timestamp] of Object.entries(rk)) {
// ignore all entries made by _doRetrieve()
if (typeof timestamp !== 'boolean') {
if (timestamp <= ts) delete rk[id];
}
}
}
/**
* update data on initialize
* returns {Promise<void>}
*/
initialize() {
return _doRetrieve.call(this, 'onInitialize');
}
/**
* update data on update
* @returns {Promise<void>}
*/
update() {
return _doRetrieve.call(this, 'onUpdate');
}
/**
* update data on activate
* @returns {Promise<void>}
*/
activate() {
return _doRetrieve.call(this, 'onActivate');
}
/**
* called after a retrieve operation completed; retrieve will await this function
* @param {string} key - the value that has changed
* @returns {Promise<void>}
* @protected
*/
async _onRetrieveReady(key) {}
/**
* called after a retrieve operation completed
* @param {string} key - the value that has changed
* @protected
*/
_onAfterRetrieve(key) {}
/**
* allows to iterate the existing data
* @generator
* @yields [number, *]}
*/
*entries() {
const classConfig = this.____getClassConfig();
const data = this.____getData();
const keys = Object.keys(classConfig?.data || {});
for (const key of keys) {
yield [key, data[key]];
}
}
/**
* @override
* @param {Help4.jscore.DataBase.Params} level0
* @param {string} type
* @param {*} value
*/
____mergeDerivedParam(level0, type, value) {
switch (type) {
case 'data': // for Help4.jscore.DataBase
level0[type] = this.____extendParams(level0[type] || {}, value);
break;
case 'config':
case 'retrieve': // for Help4.jscore.DataBase
level0[type] = Help4.extendObject(level0[type] || {}, value);
break;
default:
super.____mergeDerivedParam(level0, type, value);
break;
}
}
/**
* @override
* @param {Object} params
* @throws {Error}
* @protected
*/
____defineProperties(params) {
const {DATA_KEYS, CUSTOM_TYPE, GET, SET} = this.constructor;
super.____defineProperties(params, {GET, SET});
const classConfig = this.____getClassConfig();
const dataKeys = classConfig[DATA_KEYS] = Object.keys(classConfig.data || {});
for (const key of dataKeys) {
if (this.hasOwnProperty(key)) throw new Error(`Property "${key}" already defined!`);
const info = classConfig.data[key];
if (info.hasOwnProperty('private')) throw new Error(`"private" not allowed for data property "${key}"!`);
if (info.hasOwnProperty('readonly')) throw new Error(`"readonly" not allowed for data property "${key}"!`);
if (info.hasOwnProperty('mandatory')) throw new Error(`"mandatory" not allowed for data property "${key}"!`);
const {type, get, set} = info;
if (!type) throw new Error(`Property "${key}" need to define a type!`);
const funGet = type === CUSTOM_TYPE ? get : GET[type];
const funSet = type === CUSTOM_TYPE ? set : SET[type];
if (funGet && funSet) {
this.____defineProperty({type: 'data', key, info, funGet, funSet});
} else {
throw new Error(`Invalid type "${type}" for property or missing getter or setter!`);
}
this.____setInitValue({key, info, params, funSet});
}
}
/**
* @override
* @param {Object} args
* @param {string} args.key
* @param {Help4.jscore.ControlBase.Param} args.info
* @param {Object} args.params
* @param {Help4.jscore.ControlBase.FunSet} args.funSet
* @protected
*/
____setInitValue({key, info, params, funSet}) {
const data = this.____getData();
const {TYPES} = this.constructor;
if (!data.hasOwnProperty(key) && info.type === TYPES.dataContainer) {
data[key] = _createDataContainer.call(this, key, info);
} else {
super.____setInitValue({key, info, params, funSet});
}
}
}
/**
* @memberof Help4.jscore.DataBase#
* @param {string} key
* @param {Help4.jscore.ControlBase.Param} info
* @returns {Help4.jscore.DataContainer}
* @private
*/
function _createDataContainer(key, info) {
const container = new Help4.jscore.DataContainer(info.init);
// observe item data changes
container.onChange.addListener(event => {
/** @type {Help4.jscore.ControlBase.PropertyChangeEvent} */
event = {
target: this,
name: key,
value: event.value,
oldValue: event.oldValue
};
this.____getClassConfig().config?.onPropertyChange?.(event); // sync!
this.dataFunctions.onChange.onEvent(event); // async!
});
return container;
}
/**
* @memberof Help4.jscore.DataBase#
* @param {string} mode
* @returns {Promise<void>}
* @private
*/
async function _doRetrieve(mode) {
const {RETRIEVE_KEY} = this.constructor;
const key = `_${mode}`;
const rk = this.____getClassConfig()[RETRIEVE_KEY];
if (rk[key]) this.clean(); // an old job is still running; end it
rk[key] = true;
await _retrieve.call(this, mode);
delete rk[key];
}
/**
* @memberof Help4.jscore.DataBase#
* @param {string} mode
* @returns {Promise<void>}
* @private
*/
async function _retrieve(mode) {
const {RETRIEVE_KEY} = this.constructor;
const classConfig = this.____getClassConfig();
const rk = classConfig[RETRIEVE_KEY];
const id = Help4.createId();
rk[id] = new Date().getTime(); // allow clean() to discard all not-yet collected data
function *generateFromRetrieve() {
const {data, retrieve} = classConfig;
for (const [key, {retrieve: r}] of Object.entries(data)) {
if (r?.[mode] && retrieve[key]) {
yield {promise: retrieve[key](), key};
// clean() has been called; stop execution
if (!rk[id]) return;
}
}
}
const {map} = await Help4.awaitPromises(generateFromRetrieve);
// awaitPromises is async; clean() could have been called in between
// only use this value in case our timestamp still exists
if (rk[id] && !this.isDestroyed()) {
delete rk[id];
const dataConfig = this.____getClassConfig()['data'];
const {TYPES: {dataContainer}} = this.constructor;
const promises = /** @type {Array<Promise<void>>} */ [];
Object.keys(map).forEach(key => {
/** @type {Help4.jscore.DataBase.Data} */
const config = dataConfig[key];
const value = map[key];
if (config.type === dataContainer) {
// do not use setter for dataContainer
const container = this[key];
value
? container.set(...value)
: container.clean();
} else {
this[key] = value;
}
promises.push(this._onRetrieveReady(key));
this._onAfterRetrieve(key);
});
await Help4.Promise.all(promises);
}
}
})();