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,8 +16,7 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* 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/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
module.exports = class BillingModule extends use.Module {
|
extension.on('install', ({ services }) => {
|
||||||
install (context) {
|
|
||||||
const services = context.get('services');
|
const services = context.get('services');
|
||||||
|
|
||||||
const { CustomPuterService } = require('./CustomPuterService.js');
|
const { CustomPuterService } = require('./CustomPuterService.js');
|
||||||
@ -25,5 +24,4 @@ module.exports = class BillingModule extends use.Module {
|
|||||||
|
|
||||||
const { ShareTestService } = require('./ShareTestService.js');
|
const { ShareTestService } = require('./ShareTestService.js');
|
||||||
services.registerService('__share-test', ShareTestService);
|
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 BaseService = require("./services/BaseService");
|
||||||
const useapi = require('useapi');
|
const useapi = require('useapi');
|
||||||
const yargs = require('yargs/yargs')
|
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 {
|
class Kernel extends AdvancedBase {
|
||||||
@ -227,24 +229,38 @@ class Kernel extends AdvancedBase {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mod_class = this.useapi.withuse(() => require(mod_path));
|
const mod = new ExtensionModule();
|
||||||
const mod = new mod_class();
|
mod.extension = new Extension();
|
||||||
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, {
|
const mod_context = this._create_mod_context(mod_install_root_context, {
|
||||||
name: mod_class.name ?? mod_dirname,
|
name: mod_dirname,
|
||||||
['module']: mod,
|
['module']: mod,
|
||||||
external: true,
|
external: true,
|
||||||
mod_path,
|
mod_path,
|
||||||
});
|
});
|
||||||
|
|
||||||
if ( mod.install ) {
|
// TODO: DRY `awithuse` and `aglobalwith` with above
|
||||||
this.useapi.awithuse(async () => {
|
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);
|
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
|
// We export some things on the function itself
|
||||||
useapi.globalwith = globalwith;
|
useapi.globalwith = globalwith;
|
||||||
|
useapi.aglobalwith = aglobalwith;
|
||||||
|
|
||||||
module.exports = useapi;
|
module.exports = useapi;
|
||||||
|
Loading…
Reference in New Issue
Block a user