Ver Fonte

refactor: add traits and services as drivers

KernelDeimos há 10 meses atrás
pai
commit
2cd68100d2

+ 3 - 0
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 }) => {

+ 27 - 1
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 <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] ) {

+ 25 - 0
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 };

+ 14 - 0
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`);

+ 0 - 1
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' }));

+ 68 - 16
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 });
         }

+ 1 - 0
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'),
     ]
 }
 

+ 6 - 5
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 = {

+ 20 - 0
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);
+    },
+};