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.
This commit is contained in:
KernelDeimos 2024-10-17 18:59:03 -04:00 committed by Eric Dubé
parent 2a81825a6b
commit 273a51fc53
4 changed files with 208 additions and 0 deletions

View File

@ -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'),

View File

@ -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 = {

View File

@ -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,
};

View File

@ -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');
});
});