diff --git a/src/putility/index.js b/src/putility/index.js index 555d8504..1acc8221 100644 --- a/src/putility/index.js +++ b/src/putility/index.js @@ -18,9 +18,13 @@ */ const { AdvancedBase } = require('./src/AdvancedBase'); const { Service } = require('./src/concepts/Service'); +const { ServiceManager } = require('./src/system/ServiceManager'); module.exports = { AdvancedBase, + system: { + ServiceManager, + }, libs: { promise: require('./src/libs/promise'), context: require('./src/libs/context'), diff --git a/src/putility/src/concepts/Service.js b/src/putility/src/concepts/Service.js index 023beb78..57b0ba46 100644 --- a/src/putility/src/concepts/Service.js +++ b/src/putility/src/concepts/Service.js @@ -19,6 +19,25 @@ class Service extends AdvancedBase { || this.constructor[`__on_${id}`]?.bind?.(this.constructor) || NOOP; } + + static create ({ parameters, context }) { + const ins = new this(); + ins._.context = context; + ins.construct(parameters); + return ins; + } + + init (...a) { + if ( ! this._init ) return; + return this._init(...a); + } + + construct (o) { + this.$parameters = {}; + for ( const k in o ) this.$parameters[k] = o[k]; + if ( ! this._construct ) return; + return this._construct(o); + } } module.exports = { diff --git a/src/putility/src/system/ServiceManager.js b/src/putility/src/system/ServiceManager.js new file mode 100644 index 00000000..95c5cbbc --- /dev/null +++ b/src/putility/src/system/ServiceManager.js @@ -0,0 +1,120 @@ +const { AdvancedBase } = require("../AdvancedBase"); + +const mkstatus = name => { + const c = class { + get label () { return name } + describe () { return name } + } + c.name = `Status${ + name[0].toUpperCase() + name.slice(1) + }` + return c; +} + +class ServiceManager extends AdvancedBase { + static StatusRegistering = mkstatus('registering'); + static StatusPending = class StatusPending { + constructor ({ waiting_for }) { + this.waiting_for = waiting_for; + } + get label () { return 'waiting'; } + // TODO: trait? + describe () { + return `waiting for: ${this.waiting_for.join(', ')}` + } + } + static StatusInitializing = mkstatus('initializing'); + static StatusRunning = class StatusRunning { + constructor ({ start_ts }) { + this.start_ts = start_ts; + } + get label () { return 'running'; } + describe () { + return `running (since ${this.start_ts})`; + } + } + constructor () { + super(); + + this.services_l_ = []; + this.services_m_ = {}; + this.service_infos_ = {}; + + 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 = {}) { + const ins = factory.create({ + parameters: options.parameters ?? {}, + }); + const entry = { + name, + instance: ins, + status: new this.constructor.StatusRegistering(), + }; + this.services_l_.push(entry); + this.services_m_[name] = entry; + + await this.maybe_init_(name); + } + info (name) { + return this.services_m_[name]; + } + + async maybe_init_ (name) { + const entry = this.services_m_[name]; + const depends = entry.instance.get_depends(); + const waiting_for = []; + for ( const depend of depends ) { + const depend_entry = this.services_m_[depend]; + if ( ! depend_entry ) { + waiting_for.push(depend); + continue; + } + if ( ! (depend_entry.status instanceof this.constructor.StatusRunning) ) { + waiting_for.push(depend); + } + } + + if ( waiting_for.length === 0 ) { + await this.init_service_(name); + return; + } + + for ( const dependency of waiting_for ) { + /** @type Set */ + const waiting_set = this.waiting_[dependency] || + (this.waiting_[dependency] = new Set()); + waiting_set.add(name); + } + + entry.status = new this.constructor.StatusPending( + { waiting_for }); + } + + // called when a service has all of its dependencies initialized + // and is ready to be initialized itself + async init_service_ (name) { + const entry = this.services_m_[name]; + entry.status = new this.constructor.StatusInitializing(); + await entry.instance.init(); + entry.status = new this.constructor.StatusRunning({ + start_ts: new Date(), + }); + /** @type Set */ + const maybe_ready_set = this.waiting_[name]; + const promises = []; + if ( maybe_ready_set ) { + for ( const dependent of maybe_ready_set.values() ) { + promises.push(this.maybe_init_(dependent)); + } + } + await Promise.all(promises); + } +} + +module.exports = { + ServiceManager, +}; diff --git a/src/putility/test/ServiceManager.test.js b/src/putility/test/ServiceManager.test.js new file mode 100644 index 00000000..2219ded0 --- /dev/null +++ b/src/putility/test/ServiceManager.test.js @@ -0,0 +1,65 @@ +const { expect } = require('chai'); +const { Service } = require('../src/concepts/Service.js'); +const { ServiceManager } = require('../src/system/ServiceManager.js'); + +class TestService extends Service { + _construct ({ name, depends }) { + this.name_ = name; + this.depends_ = depends; + this.initialized_ = false; + } + get_depends () { + return this.depends_; + } + async _init () { + // to ensure init is correctly awaited in tests + await new Promise(rslv => setTimeout(rslv, 0)); + + this.initialized_ = true; + } +} + +describe('ServiceManager', () => { + it('handles dependencies', async () => { + const serviceMgr = new ServiceManager(); + + // register a service with two depends; it will start last + await serviceMgr.register('a', TestService, { + parameters: { + name: 'a', + depends: ['b', 'c'], + }, + }); + + let a_info = serviceMgr.info('a'); + expect(a_info.status.describe()).to.equal('waiting for: b, c'); + + // register a service with no depends; should start right away + await serviceMgr.register('b', TestService, { + parameters: { + name: 'b', + depends: [] + } + }); + + let b_info = serviceMgr.info('b'); + expect(b_info.status.label).to.equal('running'); + + a_info = serviceMgr.info('a'); + expect(a_info.status.describe()).to.equal('waiting for: c'); + + await serviceMgr.register('c', TestService, { + parameters: { + name: 'c', + depends: ['b'] + } + }); + + let c_info = serviceMgr.info('c'); + expect(c_info.status.label).to.equal('running'); + a_info = serviceMgr.info('a'); + expect(a_info.status.label).to.equal('running'); + b_info = serviceMgr.info('b'); + expect(b_info.status.label).to.equal('running'); + }); +});