From 2cd68100d2683ccd2f4f96b107bf96240b9c81aa Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Wed, 17 Jul 2024 23:20:26 -0400 Subject: [PATCH] refactor: add traits and services as drivers --- src/backend/src/CoreModule.js | 3 + src/backend/src/services/Container.js | 28 ++++++- src/backend/src/services/HelloWorldService.js | 25 ++++++ src/backend/src/services/RegistryService.js | 14 ++++ src/backend/src/services/SelfhostedService.js | 1 - .../src/services/drivers/DriverService.js | 84 +++++++++++++++---- src/puter-js-common/src/AdvancedBase.js | 1 + src/puter-js-common/src/bases/FeatureBase.js | 11 +-- .../src/features/TraitsFeature.js | 20 +++++ 9 files changed, 164 insertions(+), 23 deletions(-) create mode 100644 src/backend/src/services/HelloWorldService.js create mode 100644 src/puter-js-common/src/features/TraitsFeature.js diff --git a/src/backend/src/CoreModule.js b/src/backend/src/CoreModule.js index 0a28490a..30d5c97c 100644 --- a/src/backend/src/CoreModule.js +++ b/src/backend/src/CoreModule.js @@ -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 }) => { diff --git a/src/backend/src/services/Container.js b/src/backend/src/services/Container.js index d496ffec..d9ba8b1e 100644 --- a/src/backend/src/services/Container.js +++ b/src/backend/src/services/Container.js @@ -16,6 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +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] ) { diff --git a/src/backend/src/services/HelloWorldService.js b/src/backend/src/services/HelloWorldService.js new file mode 100644 index 00000000..efc0a568 --- /dev/null +++ b/src/backend/src/services/HelloWorldService.js @@ -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 }; diff --git a/src/backend/src/services/RegistryService.js b/src/backend/src/services/RegistryService.js index 2b611f82..cd966727 100644 --- a/src/backend/src/services/RegistryService.js +++ b/src/backend/src/services/RegistryService.js @@ -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`); diff --git a/src/backend/src/services/SelfhostedService.js b/src/backend/src/services/SelfhostedService.js index bfc95232..fda861d6 100644 --- a/src/backend/src/services/SelfhostedService.js +++ b/src/backend/src/services/SelfhostedService.js @@ -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' })); diff --git a/src/backend/src/services/drivers/DriverService.js b/src/backend/src/services/drivers/DriverService.js index 8dbd898e..861957c3 100644 --- a/src/backend/src/services/drivers/DriverService.js +++ b/src/backend/src/services/drivers/DriverService.js @@ -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 }); } diff --git a/src/puter-js-common/src/AdvancedBase.js b/src/puter-js-common/src/AdvancedBase.js index 4732410a..3712a8a4 100644 --- a/src/puter-js-common/src/AdvancedBase.js +++ b/src/puter-js-common/src/AdvancedBase.js @@ -25,6 +25,7 @@ class AdvancedBase extends FeatureBase { static FEATURES = [ require('./features/NodeModuleDIFeature'), require('./features/PropertiesFeature'), + require('./features/TraitsFeature'), ] } diff --git a/src/puter-js-common/src/bases/FeatureBase.js b/src/puter-js-common/src/bases/FeatureBase.js index ab7f4fb3..5ef78b73 100644 --- a/src/puter-js-common/src/bases/FeatureBase.js +++ b/src/puter-js-common/src/bases/FeatureBase.js @@ -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 = { diff --git a/src/puter-js-common/src/features/TraitsFeature.js b/src/puter-js-common/src/features/TraitsFeature.js new file mode 100644 index 00000000..68b4ac5f --- /dev/null +++ b/src/puter-js-common/src/features/TraitsFeature.js @@ -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); + }, +};