beyond
Version:
The Full Stack Universal Typescript Framework
280 lines (222 loc) • 8.58 kB
JavaScript
const {EventEmitter} = require('events');
const Nothing = class {
};
let autoincremental = {id: 0, request: 0};
// A registry of all Dynamic Processors created across the instance of the engine
const registry = new Set();
// noinspection JSPotentiallyInvalidUsageOfThis
module.exports = (Base = Nothing) => class extends Base {
_events = new EventEmitter();
on = (event, listener) => {
// To find if a dynamic processor hasn't set the maxListeners correctly
const count = this._events.listenerCount(event);
const max = this._events.getMaxListeners();
if (max === count) {
const message = `Max. listeners (${max}) achieved on dp "${this.dp}" - with id: "${this.id}"`;
this.#logs.append(message);
console.log(`${message}.\nCheck the logs: ${this.#logs.store}\n`);
const consumers = (() => {
let consumers = '';
let count = 0;
registry.forEach((consumer) => {
const {items: requiring} = consumer.children.monitor;
if (!requiring.has(this)) return;
const {dp, id} = consumer;
consumers += `\t* [${++count}] - dp "${dp}" - with id: "${id}"\n`;
});
return consumers;
})();
this.#logs.append(consumers);
}
this._events.on(event, listener);
};
off = (event, listener) => this._events.off(event, listener);
removeALlListeners = () => this._events.removeAllListeners();
setMaxListeners = n => this._events.setMaxListeners(n);
getMaxListeners = () => this._events.getMaxListeners();
listenerCount = event => this._events.listenerCount(event);
// ms to wait to process after invalidation
waitToProcess = 0;
// Execute _notify method on first processing
notifyOnFirst = false;
#autoincremented = autoincremental.id++;
get autoincremented() {
return this.#autoincremented;
}
#children;
get children() {
return this.#children;
}
#logs;
/**
* Dynamic processor setup
*
* @param children {Map<string, {child: object}>} A map of children properties
* where the key is the child object and the value is the specification object
*/
setup(children) {
this.#children.register(children, false, true);
}
// Is a property that is defined only when processing and before initialised
#ready = Promise.pending();
get ready() {
if (this.#processed || this.#destroyed) return Promise.resolve();
this.#ready = this.#ready || Promise.pending();
if (!this.#initialising && !this.#initialised) {
// Initialization triggers processing, and promise resolution
this.initialise().catch(exc => console.error(exc.stack));
}
return this.#ready.value;
}
constructor(...params) {
super(...params);
registry.add(this);
this.#children = new (require('./children'))(this, this.#preprocess);
this.setMaxListeners(500);
this.#logs = require('./logs');
}
get waiting() {
return this.#children.waiting;
}
#initialising = false;
get initialising() {
return this.#initialising;
}
#initialised = false;
get initialised() {
return this.#initialised;
}
// This method can be overridden
async _begin() {
}
async initialise() {
if (typeof this.dp !== 'string' || !this.dp) throw new Error('Getter .dp must return a string');
if (this.#destroyed || this.#initialising || this.#initialised) return;
this.#initialising = true;
await this._begin();
this.#initialising = false;
this.#initialised = true;
// On children initialisation, and after all child objects are ready, the #preprocess method is called
this.#children.initialise();
}
// The processor is processing, specifically in the preparation phase
#preparing;
get preparing() {
return this.#preparing;
}
// This method should be overridden
_prepared() {
}
// Is the processor prepared to process?
// If not prepared, the promise will be kept pending, and will be processed at the next invalidation.
get __prepared() {
this.#preparing = true;
this.#children.reset();
// Check if dynamic processor is processed, but also initialise it if it wasn't previously initialised
const require = (dp, id) => {
this.#children.require(dp, {id});
return dp.processed;
};
let prepared = this._prepared(require);
if (typeof prepared === 'string') {
this.#children.monitor.hang(prepared);
prepared = false;
}
prepared = prepared === void 0 ? true : !!prepared;
this.#preparing = false;
return prepared;
}
#first = true; // Is it the first notification?
get first() {
return this.#first;
}
// This method should be overridden
_notify() {
}
#processing = false;
get processing() {
return this.#processing;
}
#processed = false;
get processed() {
return this.#processed;
}
// This method should be overridden
async _process(request) {
void (request);
}
#tu;
get tu() {
return this.#tu;
}
#request;
get _request() {
return this.#request;
}
/**
* Called by children when ready or upon a change in any of the children, or upon invalidation
*/
#preprocess = () => {
if (this.#destroyed) return;
this.#processed = false;
this.#processing = true;
const prepared = this.__prepared;
const changed = this.#children.update();
if (changed) {
// The processor became invalid while preparing, so call preprocess again
// In this case __prepared is called again until all children is properly set
this.#preprocess();
return;
}
// If not prepared, the children is responsible to call #preprocess again when ready
if (!prepared || !this.#children.prepared) return;
const request = this.#request = autoincremental.request++;
const performance = {
now: Date.now(),
check: () => {
const ms = Date.now() - performance.now;
ms > 2000 && this.#logs.append(`"${this.dp}" took ${ms} ms. to process`);
}
};
/**
* Once process is completed
*
* @param pr? {boolean | {changed: boolean, notify: boolean}} The process response
*/
const done = (pr) => {
if (this.#request !== request) return;
pr = typeof pr === 'object' ? pr : {notify: pr, changed: pr};
pr.notify = pr.notify === void 0 ? true : !!pr.notify;
pr.changed = pr.changed === void 0 ? true : !!pr.changed;
this.#tu = Date.now(); // The time updated
performance.check();
this.#processing = false;
this.#processed = !this.#destroyed;
const ready = this.#ready;
this.#ready = void 0;
ready?.resolve();
const {changed, notify} = pr;
(changed || this.#destroyed || this.#first) && this._events.emit('change', this);
!this.#destroyed && notify && (!this.#first || this.notifyOnFirst) && this._notify();
this.#first = false;
}
// The process response
const pr = this._process(request);
pr instanceof Promise ? pr.then(done).catch(exc => console.error(exc.stack)) : done(pr);
}
_invalidate = () => {
this.#initialised && !this.#preparing && this.#preprocess();
}
#destroyed = false;
get destroyed() {
return this.#destroyed;
}
destroy() {
if (this.#destroyed) throw new Error('Object is already destroyed');
this.#request = Date.now();
this.#children.destroy();
this.#destroyed = true;
registry.delete(this);
this._events.emit('change', this);
}
}