From 273a51fc5389610f2f93278c351ee8562bbc7ac9 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Thu, 17 Oct 2024 18:59:03 -0400 Subject: [PATCH] dev: add ServiceManager The ServiceManager will replace manual management of services within initgui, and will also be used within puter.js. Eventually Puter's backend might use this instead of the existing Container class, although this will be a large change that needs to be done incrementally. The difference between ServiceManager and Container is the logic behind when initialization occurs. Rather than have all services initialized at once when Container's init() method is called, services are initialized as soon as their dependencies have been initialized. --- src/putility/index.js | 4 + src/putility/src/concepts/Service.js | 19 ++++ src/putility/src/system/ServiceManager.js | 120 ++++++++++++++++++++++ src/putility/test/ServiceManager.test.js | 65 ++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 src/putility/src/system/ServiceManager.js create mode 100644 src/putility/test/ServiceManager.test.js diff --git a/src/putility/index.js b/src/putility/index.js index 555d8504a..1acc82210 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 023beb780..57b0ba460 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 000000000..95c5cbbc2 --- /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 000000000..2219ded05 --- /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'); + }); +});