UNPKG

@akala/core

Version:
272 lines 9.06 kB
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