dev: hooks and sequencing functions for ServiceManager

This commit is contained in:
KernelDeimos 2024-10-18 22:32:01 -04:00
parent 2f6f2b7d7c
commit bc5d09fe31
4 changed files with 121 additions and 19 deletions

View File

@ -1,4 +1,5 @@
const { AdvancedBase } = require("../AdvancedBase");
const ServiceFeature = require("../features/ServiceFeature");
const NOOP = async () => {};
@ -11,6 +12,10 @@ const TService = Symbol('TService');
* becoming the common base for both and a useful utility in general.
*/
class Service extends AdvancedBase {
static FEATURES = [
ServiceFeature,
];
async __on (id, args) {
const handler = this.__get_event_handler(id);
@ -48,7 +53,10 @@ class Service extends AdvancedBase {
return this._construct(o);
},
get_depends () {
return this.get_depends?.() ?? [];
return [
...(this.constructor.DEPENDS ?? []),
...(this.get_depends?.() ?? []),
];
}
}
}

View File

@ -36,20 +36,18 @@ module.exports = {
};
instance._.properties[k] = state;
let spec = null;
if ( typeof properties[k] === 'object' ) {
// This will be supported in the future.
throw new Error(`Property ${k} in ${instance.constructor.name} ` +
`is not a supported property specification.`);
spec = properties[k];
} else if ( typeof properties[k] === 'function' ) {
spec = {};
spec.value = properties[k]();
}
let value = (() => {
if ( typeof properties[k] === 'function' ) {
return properties[k]();
if ( spec === null ) {
throw new Error('this will never happen');
}
return properties[k];
})();
Object.defineProperty(instance, k, {
get: () => {
return state.value;
@ -60,11 +58,17 @@ module.exports = {
old_value: instance[k],
});
}
const old_value = instance[k];
state.value = value;
if ( spec.post_set ) {
spec.post_set.call(instance, value, {
old_value,
});
}
},
});
state.value = value;
state.value = spec.value;
}
}
}

View File

@ -0,0 +1,28 @@
const { TTopics } = require("../traits/traits");
module.exports = {
install_in_instance: (instance, { parameters }) => {
// Convenient definition of listeners between services,
// which also makes these connections able to be understood as data
// without processing any code.
const hooks = instance._get_merged_static_array('HOOKS');
instance._.init_hooks = instance._.init_hooks ?? [];
for ( const spec of hooks ) {
// We need to wait for the service to be initialized, because
// that's when the dependency services have already been
// initialized and are ready to accept listeners.
instance._.init_hooks.push(() => {
const service_entry =
instance._.context.services.info(spec.service);
const service_instance = service_entry.instance;
service_instance.as(TTopics).sub(
spec.event,
spec.do.bind(instance),
);
});
}
}
};

View File

@ -1,5 +1,6 @@
const { AdvancedBase } = require("../AdvancedBase");
const { TService } = require("../concepts/Service");
const { TeePromise } = require("../libs/promise");
const mkstatus = name => {
const c = class {
@ -34,21 +35,26 @@ class ServiceManager extends AdvancedBase {
return `running (since ${this.start_ts})`;
}
}
constructor () {
constructor ({ context }) {
super();
this.context = context;
this.services_l_ = [];
this.services_m_ = {};
this.service_infos_ = {};
this.init_listeners_ = {};
this.init_listeners_ = [];
// services which are waiting for dependency servicces to be
// initialized; mapped like: waiting_[dependency] = Set(dependents)
this.waiting_ = {};
}
async register (name, factory, options = {}) {
await new Promise(rslv => setTimeout(rslv, 0));
const ins = factory.create({
parameters: options.parameters ?? {},
context: this.context,
});
const entry = {
name,
@ -63,10 +69,51 @@ class ServiceManager extends AdvancedBase {
info (name) {
return this.services_m_[name];
}
get (name) {
const info = this.services_m_[name];
if ( ! info ) throw new Error(`Service not registered: ${name}`);
if ( ! (info.status instanceof this.constructor.StatusRunning ) ) {
return undefined;
}
return info.instance;
}
async maybe_init_ (name) {
const entry = this.services_m_[name];
const depends = entry.instance.as(TService).get_depends();
/**
* Wait for the specified list of services to be initialized.
* @param {*} depends - list of services to wait for
*/
async wait_for_init (depends) {
let check;
await new Promise(rslv => {
check = () => {
// Get the list of required services that are not
// yet initialized
const waiting_for = this.get_waiting_for_(depends);
console.log('CHECK --- ', waiting_for, new Error());
// If there's nothing to wait for, remove the listener
// on service initializations and resolve
if ( waiting_for.length === 0 ) {
const i = this.init_listeners_.indexOf(check);
if ( i !== -1 ) {
this.init_listeners_.splice(i, 1);
}
rslv();
return true;
}
};
// Services might already be registered
if ( check() ) return;
this.init_listeners_.push(check);
});
};
get_waiting_for_ (depends) {
const waiting_for = [];
for ( const depend of depends ) {
const depend_entry = this.services_m_[depend];
@ -78,6 +125,13 @@ class ServiceManager extends AdvancedBase {
waiting_for.push(depend);
}
}
return waiting_for;
}
async maybe_init_ (name) {
const entry = this.services_m_[name];
const depends = entry.instance.as(TService).get_depends();
const waiting_for = this.get_waiting_for_(depends);
if ( waiting_for.length === 0 ) {
await this.init_service_(name);
@ -97,7 +151,7 @@ class ServiceManager extends AdvancedBase {
// called when a service has all of its dependencies initialized
// and is ready to be initialized itself
async init_service_ (name) {
async init_service_ (name, modifiers = {}) {
const entry = this.services_m_[name];
entry.status = new this.constructor.StatusInitializing();
@ -111,10 +165,18 @@ class ServiceManager extends AdvancedBase {
const promises = [];
if ( maybe_ready_set ) {
for ( const dependent of maybe_ready_set.values() ) {
promises.push(this.maybe_init_(dependent));
promises.push(this.maybe_init_(dependent, {
no_init_listeners: true
}));
}
}
await Promise.all(promises);
if ( ! modifiers.no_init_listeners ) {
for ( const lis of this.init_listeners_ ) {
await lis();
}
}
}
}