mirror of
https://github.com/HeyPuter/puter
synced 2024-11-14 22:06:00 +00:00
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:
parent
2a81825a6b
commit
273a51fc53
@ -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'),
|
||||
|
@ -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 = {
|
||||
|
120
src/putility/src/system/ServiceManager.js
Normal file
120
src/putility/src/system/ServiceManager.js
Normal 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,
|
||||
};
|
65
src/putility/test/ServiceManager.test.js
Normal file
65
src/putility/test/ServiceManager.test.js
Normal 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');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user