From bc5d09fe3167f7a2eb717bcb267469dd0e67318b Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Fri, 18 Oct 2024 22:32:01 -0400 Subject: [PATCH] dev: hooks and sequencing functions for ServiceManager --- src/putility/src/concepts/Service.js | 10 ++- .../src/features/PropertiesFeature.js | 26 ++++--- src/putility/src/features/ServiceFeature.js | 28 +++++++ src/putility/src/system/ServiceManager.js | 76 +++++++++++++++++-- 4 files changed, 121 insertions(+), 19 deletions(-) create mode 100644 src/putility/src/features/ServiceFeature.js diff --git a/src/putility/src/concepts/Service.js b/src/putility/src/concepts/Service.js index b914b598..9c92bca5 100644 --- a/src/putility/src/concepts/Service.js +++ b/src/putility/src/concepts/Service.js @@ -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?.() ?? []), + ]; } } } diff --git a/src/putility/src/features/PropertiesFeature.js b/src/putility/src/features/PropertiesFeature.js index b5ec2b22..56e27db6 100644 --- a/src/putility/src/features/PropertiesFeature.js +++ b/src/putility/src/features/PropertiesFeature.js @@ -36,19 +36,17 @@ 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](); - } - - return properties[k]; - })(); + if ( spec === null ) { + throw new Error('this will never happen'); + } Object.defineProperty(instance, k, { get: () => { @@ -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; } } } diff --git a/src/putility/src/features/ServiceFeature.js b/src/putility/src/features/ServiceFeature.js new file mode 100644 index 00000000..2075b19f --- /dev/null +++ b/src/putility/src/features/ServiceFeature.js @@ -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), + ); + }); + } + } +}; diff --git a/src/putility/src/system/ServiceManager.js b/src/putility/src/system/ServiceManager.js index ac786191..b1829986 100644 --- a/src/putility/src/system/ServiceManager.js +++ b/src/putility/src/system/ServiceManager.js @@ -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(); + } + } } }