dev: extension registry and lifecycle

Adds `register`, `registry`, `preinit`, and related symbols to the
available extension globals.
This commit is contained in:
KernelDeimos
2025-09-11 21:02:32 -04:00
parent 9135632129
commit 55c53d9eee
3 changed files with 153 additions and 6 deletions
+100 -1
View File
@@ -54,6 +54,27 @@ class Extension extends AdvancedBase {
this.log_context[lvl](...a);
}
});
this.only_one_preinit_fn = null;
this.only_one_init_fn = null;
this.registry = {
register: this.register.bind(this),
of: (typeKey) => {
return {
named: name => {
if ( arguments.length === 0 ) {
return this.registry_[typeKey].named;
}
return this.registry_[typeKey].named[name];
},
all: () => [
...Object.values(this.registry_[typeKey].named),
...this.registry_[typeKey].anonymous,
],
}
}
};
}
example () {
@@ -95,7 +116,53 @@ class Extension extends AdvancedBase {
}
return log_context;
}
/**
* Register anonymous or named data to a particular type/category.
* @param {string} typeKey Type of data being registered
* @param {string} [key] Key of data being registered
* @param {any} data The data to be registered
*/
register (typeKey, keyOrData, data) {
if ( ! this.registry_[typeKey] ) {
this.registry_[typeKey] = {
named: {},
anonymous: [],
};
}
const typeRegistry = this.registry_[typeKey];
if ( arguments.length <= 1 ) {
throw new Error('you must specify what to register');
}
if ( arguments.length === 2 ) {
data = keyOrData;
if ( Array.isArray(data) ) {
for ( const datum of data ) {
typeRegistry.anonymous.push(datum);
}
return;
}
typeRegistry.anonymous.push(data);
return;
}
const key = keyOrData;
typeRegistry.named[key] = data;
}
/**
* Alias for .register()
* @param {string} typeKey Type of data being registered
* @param {string} [key] Key of data being registered
* @param {any} data The data to be registered
*/
reg (...a) {
this.register(...a);
}
/**
* This will create a GET endpoint on the default service.
* @param {*} path - route for the endpoint
@@ -140,6 +207,38 @@ class Extension extends AdvancedBase {
});
}
preinit (callback) {
this.on('preinit', callback);
}
set preinit (callback) {
if ( this.only_one_preinit_fn === null ) {
this.on('preinit', (...a) => {
this.only_one_preinit_fn(...a);
});
}
if ( callback === null ) {
this.only_one_preinit_fn = () => {};
}
this.only_one_preinit_fn = callback;
}
init (callback) {
this.on('init', callback);
}
set init (callback) {
if ( this.only_one_init_fn === null ) {
this.on('init', (...a) => {
this.only_one_init_fn(...a);
});
}
if ( callback === null ) {
this.only_one_init_fn = () => {};
}
this.only_one_init_fn = callback;
}
//
/**
* This method will create the "default service" for an extension.
* This is specifically for Puter extensions that do not define their
+23 -4
View File
@@ -90,20 +90,27 @@ class ExtensionService extends BaseService {
const db = this.services.get('database').get(DB_WRITE, 'extension');
this.state.values.set('db', db);
// Propagate all events not from extensions to `core.`
// Propagate all events from Puter's event bus to extensions
const svc_event = this.services.get('event');
svc_event.on_all(async (key, data, meta = {}) => {
meta.from_outside_of_extension = true;
// register for both `core.` and the extension name
const promises = [];
promises.push(
this.state.extension.emit(`core.${key}`, data, meta));
// push event to the extension's event bus
promises.push(
this.state.extension.emit(key, data, meta));
// legacy: older extensions prefix "core." to events from Puter
promises.push(
this.state.extension.emit(`core.${key}`, data, meta));
// future: going to remove 'boot.' prefix from lifecycle events
await Promise.all(promises);
});
// Propagate all events from extension to Puter's event bus
this.state.extension.on_all(async (key, data, meta) => {
if ( meta.from_outside_of_extension ) return;
@@ -141,6 +148,18 @@ class ExtensionService extends BaseService {
},
});
})();
this.state.extension.emit('preinit');
}
['__on_boot.consolidation'] (...a) {
this.state.extension.emit('init', ...a);
}
['__on_boot.activation'] (...a) {
this.state.extension.emit('activate', ...a);
}
['__on_boot.ready'] (...a) {
this.state.extension.emit('ready', ...a);
}
['__on_install.routes'] (_, { app }) {
+30 -1
View File
@@ -47,6 +47,8 @@ class Kernel extends AdvancedBase {
});
this.entry_path = entry_path;
this.extensionExports = {};
this.registry = {};
}
add_module (module) {
@@ -118,6 +120,8 @@ class Kernel extends AdvancedBase {
services,
config,
logger: this.bootLogger,
extensionExports: this.extensionExports,
registry: this.registry,
args,
}, 'app');
globalThis.root_context = root_context;
@@ -222,6 +226,7 @@ class Kernel extends AdvancedBase {
globalThis.__puter_extension_globals__ = {
extensionObjectRegistry: {},
useapi: this.useapi,
global_config: require('./config'),
};
// Install the mods...
@@ -251,6 +256,7 @@ class Kernel extends AdvancedBase {
});
}
})();
if ( process.env.SYNC_MOD_INSTALL ) await p;
mod_installation_promises.push(p);
}
@@ -307,8 +313,13 @@ class Kernel extends AdvancedBase {
await prependToJSFiles(mod_package_dir, [
`const { use, def } = globalThis.__puter_extension_globals__.useapi;`,
`const { use: puter } = globalThis.__puter_extension_globals__.useapi;`,
`const extension = globalThis.__puter_extension_globals__` +
`.extensionObjectRegistry[${JSON.stringify(extension_id)}];`,
`const config = extension.config;`,
`const registry = extension.registry;`,
`const register = registry.register;`,
`const global_config = globalThis.__puter_extension_globals__.global_config`,
].join('\n') + '\n');
const mod_require_dir = path_.join(process.cwd(), mod_package_dir);
@@ -342,7 +353,25 @@ class Kernel extends AdvancedBase {
exportObject = await maybe_promise;
} else exportObject = maybe_promise;
// TODO: do something with exportObject
const extension_name = exportObject?.name ?? mod_packageJSON.name;
this.extensionExports[extension_name] = exportObject;
mod.extension.registry = this.registry;
mod.extension.name = extension_name;
if ( exportObject.construct ) {
mod.extension.on('construct', exportObject.construct);
}
if ( exportObject.preinit ) {
mod.extension.on('preinit', exportObject.preinit);
}
if ( exportObject.init ) {
mod.extension.on('init', exportObject.init);
}
Object.defineProperty(mod.extension, 'config', {
get: () => require('./config').services?.[extension_name] ?? {},
});
// This is where the 'install' event gets triggered
await mod.install(mod_context);