@akala/core
Version:
272 lines • 9.06 kB
JavaScript
import { Orchestrator } from './orchestrator.js';
import { defaultInjector, SimpleInjector } from './injectors/simple-injector.js';
import { logger } from './logging/index.browser.js';
import { Event } from './events/shared.js';
import { AsyncEvent, } from './events/async.js';
import { noop } from './helpers.js';
const orchestratorLog = logger.use('akala:module:orchestrator');
/**
* Extended event class that supports async completion tracking
* @template T - Type of event arguments
* @extends AsyncEvent<[ExtendableEvent<T>]>
*/
export class ExtendableEvent extends AsyncEvent {
once;
markAsDone;
_triggered;
/**
* Create an ExtendableEvent
* @param once - Whether the event should only trigger once
*/
constructor(once) {
super(Event.maxListeners, noop);
this.once = once;
this.reset();
}
promises = [];
/**
* Add a promise to wait for before marking event as complete
* @template T - Type of promise result
* @param p - Promise to wait for
*/
waitUntil(p) {
this.promises.push(p);
}
/**
* Reset event state for reuse (if configured with once=false)
* @throws Error if reset during incomplete event or after single use
*/
reset() {
if (!this.done && this._triggered || this.done && this.once)
throw new Error('you cannot reset an extended event if it did not complete yet');
this.promises = [];
this._done = false;
this.eventArgs = null;
this._triggered = false;
this._whenDone = new Promise((resolve) => {
this.markAsDone = resolve;
});
}
/** Event arguments passed during triggering */
eventArgs;
/**
* Trigger the event with given arguments
* @param value - Arguments to pass to event handlers
*/
async trigger(value) {
if (!this._triggered) {
this.eventArgs = value;
this._triggered = true;
await super.emit(this);
}
await this.complete();
if (!this.once)
this.reset();
}
/**
* Add event listener
* @param handler - Handler function to add
* @returns Self for chaining
*/
addListener(handler) {
if (this._done || this._triggered) {
handler(this);
}
else {
return super.addListener(handler);
}
}
/** Whether the event has been triggered */
get triggered() {
return this._triggered;
}
_whenDone;
/** Promise that resolves when event completes */
get whenDone() {
return this._whenDone;
}
/** Complete all pending promises and mark event done */
async complete() {
for (const p of this.promises) {
await p;
}
this.markAsDone();
this._done = true;
}
/** Whether the event has completed */
get done() { return this._done; }
_done;
}
/**
* Core module management class handling dependency injection and lifecycle events
* @extends SimpleInjector
*/
export class Module extends SimpleInjector {
name;
dep;
/**
* Create a new Module
* @param name - Unique module name
* @param dep - Optional array of module dependencies
*/
constructor(name, dep) {
super(moduleInjector);
this.name = name;
this.dep = dep;
const existingModule = moduleInjector.resolve(name);
if (existingModule?.dep?.length && dep?.length)
throw new Error('the module ' + existingModule.name + ' can be registered only once with dependencies');
if (existingModule) {
if (typeof (dep) != 'undefined') {
delete Module.o.tasks[name + '#activate'];
delete Module.o.tasks[name + '#ready'];
delete Module.o.tasks[name];
existingModule.dep = dep;
moduleInjector.unregister(name);
Module.registerModule(existingModule);
}
return existingModule;
}
Module.registerModule(this);
}
static o = new Orchestrator();
/** Event triggered when module activates */
activateEvent = new ExtendableEvent(true);
/** Event triggered when module is fully ready */
readyEvent = new ExtendableEvent(true);
/**
* Add a module dependency
* @param m - Module to add as dependency
*/
addDependency(m) {
if (this.dep.indexOf(m) != -1)
return;
delete Module.o.tasks[this.name + '#activate'];
delete Module.o.tasks[this.name + '#ready'];
delete Module.o.tasks[this.name];
this.dep.push(m);
moduleInjector.unregister(this.name);
Module.registerModule(this);
}
/**
* Register a module with the orchestrator
* @param m - Module to register
*/
static registerModule(m) {
if (typeof m.dep == 'undefined')
m.dep = [];
const activateDependencies = m.dep.map(dep => dep.name + '#activate');
Module.o.add(m.name + '#activate', activateDependencies, function () {
return m.activateEvent.trigger();
});
Module.o.add(m.name + '#ready', [m.name + '#activate'].concat(m.dep.map(dep => dep.name + '#ready')), function () {
return m.readyEvent.trigger();
});
Module.o.add(m.name, [m.name + '#ready']);
moduleInjector.register(m.name, m);
}
ready(toInject, f) {
if (!f)
return (f) => this.ready(toInject, f);
this.readyEvent.addListener(this.injectWithName(toInject, f));
return this;
}
readyAsync(toInject, f) {
if (!f)
return (f) => this.readyAsync(toInject, f);
this.readyEvent.addListener(this.injectWithNameAsync(toInject, f));
return this;
}
activate(toInject, f) {
if (!f)
return (f) => this.activate(toInject, f);
this.activateEvent.addListener(this.injectWithName(toInject, f));
return this;
}
activateAsync(toInject, f) {
if (!f)
return (f) => this.activateAsync(toInject, f);
this.activateEvent.addListener(this.injectWithNameAsync(toInject, f));
return this;
}
/**
* Create activation handler for class instantiation
* @param toInject - Names of dependencies to inject
* @returns Decorator function for class constructor
*/
activateNew(...toInject) {
return (ctor) => {
this.activate(toInject, function (...args) {
new ctor(...args);
});
};
}
/**
* Create async activation handler for class instantiation
* @param toInject - Names of dependencies to inject
* @returns Decorator function for class constructor
*/
activateNewAsync(...toInject) {
return function (ctor) {
this.activateAsync(toInject, function (...args) {
return new ctor(...args);
});
};
}
/**
* Create ready handler for class instantiation
* @param toInject - Names of dependencies to inject
* @returns Decorator function for class constructor
*/
readyNew(...toInject) {
return (ctor) => {
this.ready(toInject, function (...args) {
new ctor(...args);
});
};
}
/**
* Create async ready handler for class instantiation
* @param toInject - Names of dependencies to inject
* @returns Decorator function for class constructor
*/
readyNewAsync(...toInject) {
return function (ctor) {
this.readyAsync(toInject, function (...args) {
return new ctor(...args);
});
};
}
/**
* Start the module lifecycle
* @template TArgs - Argument types
* @param toInject - Names of dependencies to inject
* @param f - Optional handler function
* @returns Promise that resolves when module stops
*/
start(toInject, f) {
return new Promise((resolve, reject) => {
if (toInject?.length > 0)
Module.o.on('stop', this.injectWithName(toInject, f));
Module.o.on('task_stop', (ev) => {
if (ev.taskName === this.name)
resolve();
});
Module.o.on('error', err => reject(err.error));
Module.o.start(this.name);
});
}
}
Module['o'].on('task_start', ev => orchestratorLog.debug(ev.message));
Module['o'].on('task_stop', ev => orchestratorLog.debug(ev.message));
let moduleInjector = defaultInjector.resolve('$modules');
if (!moduleInjector) {
moduleInjector = new SimpleInjector();
defaultInjector.register('$modules', moduleInjector);
}
export function module(name, ...dependencies) {
if (dependencies?.length)
return new Module(name, dependencies.map(m => typeof (m) == 'string' ? module(m) : m));
return new Module(name);
}
//# sourceMappingURL=module.js.map