refactor: add traits and services as drivers

This commit is contained in:
KernelDeimos
2024-07-17 23:20:26 -04:00
committed by Eric Dubé
parent 8beda66328
commit 2cd68100d2
9 changed files with 164 additions and 23 deletions

View File

@@ -302,6 +302,9 @@ const install = async ({ services, app, useapi }) => {
const { AnomalyService } = require('./services/AnomalyService');
services.registerService('anomaly', AnomalyService);
const { HelloWorldService } = require('./services/HelloWorldService');
services.registerService('hello-world', HelloWorldService);
}
const install_legacy = async ({ services }) => {

View File

@@ -16,6 +16,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { AdvancedBase } = require("@heyputer/puter-js-common");
const config = require("../config");
const { Context } = require("../util/context");
const { CompositeError } = require("../util/errorutil");
@@ -26,6 +27,7 @@ class Container {
constructor ({ logger }) {
this.logger = logger;
this.instances_ = {};
this.implementors_ = {};
this.ready = new TeePromise();
}
/**
@@ -37,9 +39,24 @@ class Container {
*/
registerService (name, cls, args) {
const my_config = config.services?.[name] || {};
this.instances_[name] = cls.getInstance
const instance = cls.getInstance
? cls.getInstance({ services: this, config, my_config, name, args })
: new cls({ services: this, config, my_config, name, args }) ;
this.instances_[name] = instance;
if ( !(instance instanceof AdvancedBase) ) return;
const traits = instance.list_traits();
for ( const trait of traits ) {
if ( ! this.implementors_[trait] ) {
this.implementors_[trait] = [];
}
this.implementors_[trait].push({
name,
instance,
impl: instance.as(trait),
});
}
}
/**
* patchService allows overriding methods on a service that is already
@@ -54,6 +71,15 @@ class Container {
const patch_instance = new patch();
patch_instance.patch({ original_service, args });
}
// get_implementors returns a list of implementors for the specified
// interface name.
get_implementors (interface_name) {
const internal_list = this.implementors_[interface_name];
const clone = [...internal_list];
return clone;
}
set (name, instance) { this.instances_[name] = instance; }
get (name, opts) {
if ( this.instances_[name] ) {

View File

@@ -0,0 +1,25 @@
const BaseService = require("./BaseService");
class HelloWorldService extends BaseService {
static IMPLEMENTS = {
['driver-metadata']: {
get_response_meta () {
return {
driver: 'hello-world',
driver_version: 'v1.0.0',
driver_interface: 'helloworld',
};
}
},
helloworld: {
async greet ({ subject }) {
if ( subject ) {
return `Hello, ${subject}!`;
}
return `Hello, World!`;
}
},
}
}
module.exports = { HelloWorldService };

View File

@@ -43,6 +43,10 @@ class MapCollection extends AdvancedBase {
del (key) {
return this.kv.del(this._mk_key(key));
}
keys () {
return this.kv.keys(`registry:map:${this.map_id}:*`);
}
_mk_key (key) {
return `registry:map:${this.map_id}:${key}`;
@@ -58,6 +62,16 @@ class RegistryService extends BaseService {
this.collections_ = {};
}
async ['__on_boot.consolidation'] () {
const services = this.services;
await services.emit('registry.collections', {
svc_registry: this,
});
await services.emit('registry.entries', {
svc_registry: this,
});
}
register_collection (name) {
if ( this.collections_[name] ) {
throw Error(`collection ${name} already exists`);

View File

@@ -29,7 +29,6 @@ class SelfhostedService extends BaseService {
async _init () {
const svc_driver = this.services.get('driver');
svc_driver.register_driver('helloworld', new HelloWorld());
svc_driver.register_driver('puter-kvstore', new DBKVStore());
svc_driver.register_driver('puter-apps', new EntityStoreImplementation({ service: 'es:app' }));
svc_driver.register_driver('puter-subdomains', new EntityStoreImplementation({ service: 'es:subdomain' }));

View File

@@ -21,6 +21,7 @@ const APIError = require("../../api/APIError");
const { DriverError } = require("./DriverError");
const { TypedValue } = require("./meta/Runtime");
const BaseService = require("../BaseService");
const { Driver } = require("../../definitions/Driver");
/**
* DriverService provides the functionality of Puter drivers.
@@ -31,10 +32,30 @@ class DriverService extends BaseService {
}
_construct () {
this.interfaces = require('./interfaces');
this.drivers = {};
this.interface_to_implementation = {};
}
async ['__on_registry.collections'] (_, { svc_registry }) {
svc_registry.register_collection('interfaces');
svc_registry.register_collection('drivers');
}
async ['__on_registry.entries'] (_, { svc_registry }) {
const services = this.services;
const col_interfaces = svc_registry.get('interfaces');
const col_drivers = svc_registry.get('drivers');
{
const default_interfaces = require('./interfaces');
for ( const k in default_interfaces ) {
col_interfaces.set(k, default_interfaces[k]);
}
}
await services.emit('driver.register.interfaces',
{ col_interfaces });
await services.emit('driver.register.drivers',
{ col_drivers });
}
_init () {
const svc_registry = this.services.get('registry');
svc_registry.register_collection('');
@@ -43,9 +64,27 @@ class DriverService extends BaseService {
register_driver (interface_name, implementation) {
this.interface_to_implementation[interface_name] = implementation;
}
get_interface (interface_name) {
return this.interfaces[interface_name];
const o = {};
const col_interfaces = svc_registry.get('interfaces');
const keys = col_interfaces.keys();
for ( const k of keys ) o[k] = col_interfaces.get(k);
return col_interfaces.get(interface_name);
}
get_default_implementation (interface_name) {
// If there's a hardcoded implementation, use that
// (^ temporary, until all are migrated)
if (this.interface_to_implementation.hasOwnProperty(interface_name)) {
return this.interface_to_implementation[interface_name];
}
this.log.noticeme('HERE IT IS');
const options = this.services.get_implementors(interface_name);
this.log.info('test', { options });
if ( options.length < 1 ) return;
return options[0];
}
async call (...a) {
@@ -76,16 +115,33 @@ class DriverService extends BaseService {
throw APIError.create('permission_denied');
}
const instance = this.interface_to_implementation[interface_name];
const svc_registry = this.services.get('registry');
const c_interfaces = svc_registry.get('interfaces');
const instance = this.get_default_implementation(interface_name);
if ( ! instance ) {
throw APIError.create('no_implementation_available', null, { interface_name })
}
const meta = await instance.get_response_meta();
const sla_override = await this.maybe_get_sla(interface_name, method);
const meta = await (async () => {
if ( instance instanceof Driver ) {
return await instance.get_response_meta();
}
if ( ! instance.instance.as('driver-metadata') ) return;
const t = instance.instance.as('driver-metadata');
return t.get_response_meta();
})();
try {
let result = await instance.call(method, processed_args, sla_override);
let result;
if ( instance instanceof Driver ) {
result = await instance.call(
method, processed_args);
} else {
// TODO: SLA and monthly limits do not apply do drivers
// from service traits (yet)
result = await instance.impl[method](processed_args);
}
if ( result instanceof TypedValue ) {
const interface_ = this.interfaces[interface_name];
const interface_ = c_interfaces.get(interface_name);
let desired_type = interface_.methods[method]
.result_choices[0].type;
const svc_coercion = services.get('coercion');
@@ -127,16 +183,12 @@ class DriverService extends BaseService {
return this.interfaces;
}
async maybe_get_sla (interface_name, method) {
const services = this.services;
const fs = services.get('filesystem');
return false;
}
async _process_args (interface_name, method_name, args) {
const svc_registry = this.services.get('registry');
const c_interfaces = svc_registry.get('interfaces');
// Note: 'interface' is a strict mode reserved word.
const interface_ = this.interfaces[interface_name];
const interface_ = c_interfaces.get(interface_name);
if ( ! interface_ ) {
throw APIError.create('interface_not_found', null, { interface_name });
}

View File

@@ -25,6 +25,7 @@ class AdvancedBase extends FeatureBase {
static FEATURES = [
require('./features/NodeModuleDIFeature'),
require('./features/PropertiesFeature'),
require('./features/TraitsFeature'),
]
}

View File

@@ -21,7 +21,12 @@ const { BasicBase } = require("./BasicBase");
class FeatureBase extends BasicBase {
constructor (parameters, ...a) {
super(parameters, ...a);
for ( const feature of this.features ) {
this._ = {
features: this._get_merged_static_array('FEATURES'),
};
for ( const feature of this._.features ) {
feature.install_in_instance(
this,
{
@@ -30,10 +35,6 @@ class FeatureBase extends BasicBase {
)
}
}
get features () {
return this._get_merged_static_array('FEATURES');
}
}
module.exports = {

View File

@@ -0,0 +1,20 @@
module.exports = {
install_in_instance: (instance, { parameters }) => {
const impls = instance._get_merged_static_object('IMPLEMENTS');
instance._.impls = {};
for ( const impl_name in impls ) {
const impl = impls[impl_name];
const bound_impl = {};
for ( const method_name in impl ) {
const fn = impl[method_name];
bound_impl[method_name] = fn.bind(instance);
}
instance._.impls[impl_name] = bound_impl;
}
instance.as = trait_name => instance._.impls[trait_name];
instance.list_traits = () => Object.keys(instance._.impls);
},
};