(function() {
/**
* @namespace jscore
* @memberof Help4
*/
Help4.jscore = {};
/**
* @typedef {Object} Help4.jscore.Base.Static
* @property {*} [init = null]
* @property {boolean} [destroy = true]
*/
/**
* the class parameters
* @typedef {Object} Help4.jscore.Base.Params
* @property {?Object} [config] - configuration
* @property {?Object} [statics] - properties w/o intelligence but w/ automatic creation and cleanup; {@link Help4.jscore.Base.Static}
* @property {?Object} [derived] - hand-over of derived class params
*/
/**
* the class configuration
* @typedef {Object} Help4.jscore.Base.ClassConfig
* @property {Object} config - configuration
* @property {Object} statics - properties w/o intelligence but w/ automatic creation and cleanup; {@link Help4.jscore.Base.Static}
* @property {?Object} [derived] - hand-over of derived class params
*/
/**
* JS base class
* @abstract
* @property {Help4.jscore.Base.ClassConfig} ____classConfig - class configuration
* @property {Object} _listeners - event listeners
*/
Help4.jscore.Base = class {
/**
* @constructor
* @param {Help4.jscore.Base.Params} [classConfig]
* @param {boolean} [isDerived = false]
*/
constructor(classConfig, isDerived = false) {
classConfig ||= {};
const {PROP_NAME} = this.constructor;
this[PROP_NAME] = classConfig;
classConfig.config ||= {};
classConfig.statics ||= {};
classConfig.statics._listeners ||= {init: {}, destroy: false};
if (!isDerived) {
this.____resolveDerivedParams(classConfig);
this.____defineStaticProperties();
}
}
static PROP_NAME = '____classConfig';
/** destroy instance */
destroy() {
const {_listeners} = this;
for (const [type, {sync, async} = {}] of Object.entries(_listeners || {})) {
sync?.destroy();
async?.destroy();
delete _listeners[type];
}
const {statics} = this.____getClassConfig();
for (const [id, config] of Object.entries(statics || {})) {
if (config.destroy) this[id]?.destroy?.();
delete this[id];
}
delete this[this.constructor.PROP_NAME];
this.destroy = () => {}; // avoid multiple destruction
this.isDestroyed = () => true;
}
/**
* whether instance is destroyed
* @returns {boolean}
*/
isDestroyed() {
return false;
}
/**
* add an event listener
* @param {string|string[]} eventType - event type to be observed
* @param {Help4.EmbeddedEvent.Listener} listener - the callback function
* @returns {Help4.jscore.Base}
*/
addListener(eventType, listener) {
if (Help4.isArray(eventType)) {
eventType.forEach(type => this.addListener(type, listener));
} else {
if (typeof listener === 'function') {
const {_listeners} = this;
_listeners[eventType] ||= {};
const listeners = _listeners[eventType];
listeners.async ||= new Help4.EmbeddedEvent();
listeners.async.addListener(listener);
}
}
return this;
}
/**
* add an event listener
* @param {string|string[]} eventType - event type to be observed
* @param {Help4.EmbeddedEvent.Listener} listener - the callback function
* @returns {Help4.jscore.Base}
*/
addListenerSync(eventType, listener) {
if (Help4.isArray(eventType)) {
eventType.forEach(type => this.addListener(type, listener));
} else {
if (typeof listener === 'function') {
const {_listeners} = this;
_listeners[eventType] ||= {};
const listeners = _listeners[eventType];
listeners.sync ||= new Help4.EmbeddedEventSync();
listeners.sync.addListener(listener);
}
}
return this;
}
/**
* remove an event listener
* @param {string|string[]} eventType
* @param {Help4.EmbeddedEvent.Listener} listener
* @returns {Help4.jscore.Base}
*/
removeListener(eventType, listener) {
if (Help4.isArray(eventType)) {
eventType.forEach(type => this.removeListener(type, listener))
} else {
const {_listeners} = this;
const listeners = _listeners?.[eventType];
if (listeners) {
const listenersS = listeners.sync;
if (listenersS) {
listenersS.removeListener(listener);
if (!listenersS.countListeners()) {
listenersS.destroy();
delete listeners.sync;
}
}
const listenersA = listeners.async;
if (listenersA) {
listenersA.removeListener(listener);
if (!listenersA.countListeners()) {
listenersA.destroy();
delete listeners.async;
}
}
if (!Object.keys(listeners).length) {
delete _listeners[eventType];
}
}
}
return this;
}
/**
* fires an event that can be observed from outside
* @param {Object} event - the event to be fired
* @returns {Help4.jscore.Base}
* @protected
*/
_fireEvent(event) {
// events are sometimes async and sometimes with long execution chains
// sometimes they lead to the destruction of this class instance
event.target ||= [];
event.target.unshift(this);
const {_listeners} = this;
_listeners?.[event.type]?.async?.onEvent(event);
_listeners?.['*']?.async?.onEvent(event);
return this;
}
/**
* fires an event that can be observed from outside
* @param {Object} event - the event to be fired
* @returns {Promise<Help4.EmbeddedEvent.EventResponse[]>}
* @protected
*/
async _fireEvent2(event) {
// events are sometimes async and sometimes with long execution chains
// sometimes they lead to the destruction of this class instance
event.target ||= [];
event.target.unshift(this);
const {_listeners} = this;
/** @type {Help4.EmbeddedEvent.EventResponse[]} */
const result1 = await _listeners?.[event.type]?.async?.onEvent2(event) || [];
/** @type {Help4.EmbeddedEvent.EventResponse[]} */
const result2 = await _listeners?.['*']?.async?.onEvent2(event) || [];
return [...result1, ...result2];
}
/**
* fires an event that can be observed from outside
* @param {Object} event - the event to be fired
* @returns {Help4.EmbeddedEvent.EventResponse[]}
* @protected
*/
_fireEventSync(event) {
event.target ||= [];
event.target.unshift(this);
const {_listeners} = this;
/** @type {Help4.EmbeddedEvent.EventResponse[]} */
const result1 = _listeners?.[event.type]?.sync?.onEvent(event) || [];
/** @type {Help4.EmbeddedEvent.EventResponse[]} */
const result2 = _listeners?.['*']?.sync?.onEvent(event) || [];
return [...result1, ...result2];
}
/**
* destroys class properties
* @param {string} keys
* @protected
*/
_destroyControl(...keys) {
for (const key of keys) {
this[key]?.destroy?.();
delete this[key];
}
}
/**
* @returns {Help4.jscore.Base.Params}
* @protected
*/
____getClassConfig() {
return this[this.constructor.PROP_NAME];
}
/**
* @param {Help4.jscore.Base.Params} classConfig
* @protected
*/
____resolveDerivedParams(classConfig) {
let level0 = classConfig;
let level1 = classConfig.derived;
while (level1 && level1.derived) {
level0 = level1;
level1 = level1.derived;
}
if (level1) {
this.____mergeDerivedParams(level0, level1)
delete level0.derived;
if (classConfig.derived) this.____resolveDerivedParams(classConfig);
}
}
/**
* @param {Help4.jscore.Base.Params} level0
* @param {Help4.jscore.Base.Params} level1
* @throws {Error}
* @protected
*/
____mergeDerivedParams(level0, level1) {
for (const [type, value] of Object.entries(level1)) {
this.____mergeDerivedParam(level0, type, value);
}
}
/**
* @param {Help4.jscore.Base.Params} level0
* @param {string} type
* @param {*} value
* @throws {Error}
* @protected
*/
____mergeDerivedParam(level0, type, value) {
switch (type) {
case 'statics':
case 'derived':
level0[type] = Help4.extendObject(level0[type] || {}, value);
break;
default:
console.error(this);
throw new Error(`Unhandled derived parameter "${type}"!`);
}
}
/**
* auto define all to be cleaned properties
* @throws {Error}
* @protected
*/
____defineStaticProperties() {
const {statics} = this.____getClassConfig();
for (const [id, config] of Object.entries(statics || {})) {
if (this[id] === undefined) {
config.destroy = Help4.clampBoolean(config.destroy, true);
this[id] = config.init ?? null;
} else {
console.error(this);
throw new Error(`${id} cannot be defined twice!`);
}
}
}
}
})();