mirror of
https://github.com/HeyPuter/puter
synced 2024-11-14 22:06:00 +00:00
feat: add extension API for modules
Allows modules to register a listener to the 'install' event without creating a Module class. This changes how external modules are installed. External modules are now referred to as "extensions"; this commit does not update the term but does use 'extension' as the name of the global.
This commit is contained in:
parent
f36718005f
commit
14d45a27ed
@ -16,14 +16,12 @@
|
||||
* 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/>.
|
||||
*/
|
||||
module.exports = class BillingModule extends use.Module {
|
||||
install (context) {
|
||||
const services = context.get('services');
|
||||
extension.on('install', ({ services }) => {
|
||||
const services = context.get('services');
|
||||
|
||||
const { CustomPuterService } = require('./CustomPuterService.js');
|
||||
services.registerService('__custom-puter', CustomPuterService);
|
||||
|
||||
const { ShareTestService } = require('./ShareTestService.js');
|
||||
services.registerService('__share-test', ShareTestService);
|
||||
}
|
||||
}
|
||||
const { CustomPuterService } = require('./CustomPuterService.js');
|
||||
services.registerService('__custom-puter', CustomPuterService);
|
||||
|
||||
const { ShareTestService } = require('./ShareTestService.js');
|
||||
services.registerService('__share-test', ShareTestService);
|
||||
});
|
||||
|
19
src/backend/src/Extension.js
Normal file
19
src/backend/src/Extension.js
Normal file
@ -0,0 +1,19 @@
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
const EmitterFeature = require("@heyputer/putility/src/features/EmitterFeature");
|
||||
const { Context } = require("./util/context");
|
||||
|
||||
class Extension extends AdvancedBase {
|
||||
static FEATURES = [
|
||||
EmitterFeature({
|
||||
decorators: [
|
||||
fn => Context.get(undefined, {
|
||||
allow_fallback: true,
|
||||
}).abind(fn)
|
||||
]
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Extension,
|
||||
}
|
13
src/backend/src/ExtensionModule.js
Normal file
13
src/backend/src/ExtensionModule.js
Normal file
@ -0,0 +1,13 @@
|
||||
const { AdvancedBase } = require("@heyputer/putility");
|
||||
|
||||
class ExtensionModule extends AdvancedBase {
|
||||
async install (context) {
|
||||
const services = context.get('services');
|
||||
|
||||
this.extension.emit('install', { context, services })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ExtensionModule,
|
||||
};
|
@ -21,7 +21,9 @@ const { Context } = require('./util/context');
|
||||
const BaseService = require("./services/BaseService");
|
||||
const useapi = require('useapi');
|
||||
const yargs = require('yargs/yargs')
|
||||
const { hideBin } = require('yargs/helpers')
|
||||
const { hideBin } = require('yargs/helpers');
|
||||
const { Extension } = require("./Extension");
|
||||
const { ExtensionModule } = require("./ExtensionModule");
|
||||
|
||||
|
||||
class Kernel extends AdvancedBase {
|
||||
@ -226,25 +228,39 @@ class Kernel extends AdvancedBase {
|
||||
if ( ! stat.isDirectory() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mod = new ExtensionModule();
|
||||
mod.extension = new Extension();
|
||||
|
||||
const mod_class = this.useapi.withuse(() => require(mod_path));
|
||||
const mod = new mod_class();
|
||||
if ( ! mod ) {
|
||||
continue;
|
||||
}
|
||||
// This is where the module gets the 'use' and 'def' globals
|
||||
await this.useapi.awithuse(async () => {
|
||||
// This is where the module gets the 'extension' global
|
||||
await useapi.aglobalwith({
|
||||
extension: mod.extension,
|
||||
}, async () => {
|
||||
const maybe_promise = require(mod_path);
|
||||
if ( maybe_promise && maybe_promise instanceof Promise ) {
|
||||
await maybe_promise;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const mod_context = this._create_mod_context(mod_install_root_context, {
|
||||
name: mod_class.name ?? mod_dirname,
|
||||
name: mod_dirname,
|
||||
['module']: mod,
|
||||
external: true,
|
||||
mod_path,
|
||||
});
|
||||
|
||||
if ( mod.install ) {
|
||||
this.useapi.awithuse(async () => {
|
||||
|
||||
// TODO: DRY `awithuse` and `aglobalwith` with above
|
||||
await this.useapi.awithuse(async () => {
|
||||
await useapi.aglobalwith({
|
||||
extension: mod.extension,
|
||||
}, async () => {
|
||||
// This is where the 'install' event gets triggered
|
||||
await mod.install(mod_context);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
68
src/putility/src/features/EmitterFeature.js
Normal file
68
src/putility/src/features/EmitterFeature.js
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* A simpler alternative to TopicsFeature. This is an opt-in and not included
|
||||
* in AdvancedBase.
|
||||
*
|
||||
* Adds methods `.on` and `emit`. Unlike TopicsFeature, this does not implement
|
||||
* a trait. Usage is similar to node's built-in EventEmitter, but because it's
|
||||
* installed as a mixin it can be used with other class features.
|
||||
*
|
||||
* When listeners return a promise, they will block the promise returned by the
|
||||
* corresponding `emit()` call. Listeners are invoked concurrently, so
|
||||
* listeners of the same event do not block each other.
|
||||
*/
|
||||
module.exports = ({ decorators }) => ({
|
||||
install_in_instance (instance, { parameters }) {
|
||||
// install the internal state
|
||||
const state = instance._.emitterFeature = {};
|
||||
state.listeners_ = {};
|
||||
state.callbackDecorators = decorators || [];
|
||||
|
||||
instance.emit = async (key, data, meta) => {
|
||||
meta = meta ?? {};
|
||||
const parts = key.split('.');
|
||||
|
||||
const promises = [];
|
||||
for ( let i = 0; i < parts.length; i++ ) {
|
||||
const part = i === parts.length - 1
|
||||
? parts.join('.')
|
||||
: parts.slice(0, i + 1).join('.') + '.*';
|
||||
|
||||
// actual emit
|
||||
const listeners = state.listeners_[part];
|
||||
if ( ! listeners ) continue;
|
||||
for ( let i = 0; i < listeners.length; i++ ) {
|
||||
let callback = listeners[i];
|
||||
for ( const decorator of state.callbackDecorators ) {
|
||||
callback = decorator(callback);
|
||||
}
|
||||
|
||||
promises.push(callback(data, {
|
||||
...meta,
|
||||
key,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
instance.on = (selector, callback) => {
|
||||
const listeners = state.listeners_[selector] ||
|
||||
(state.listeners_[selector] = []);
|
||||
|
||||
listeners.push(callback);
|
||||
|
||||
const det = {
|
||||
detach: () => {
|
||||
const idx = listeners.indexOf(callback);
|
||||
if ( idx !== -1 ) {
|
||||
listeners.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return det;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -114,5 +114,6 @@ const useapi = function useapi () {
|
||||
|
||||
// We export some things on the function itself
|
||||
useapi.globalwith = globalwith;
|
||||
useapi.aglobalwith = aglobalwith;
|
||||
|
||||
module.exports = useapi;
|
||||
|
Loading…
Reference in New Issue
Block a user