From 55c53d9eee6ee117c42b52f50fac50416f50d4a8 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Thu, 11 Sep 2025 21:02:32 -0400 Subject: [PATCH] dev: extension registry and lifecycle Adds `register`, `registry`, `preinit`, and related symbols to the available extension globals. --- src/backend/src/Extension.js | 101 +++++++++++++++++++++++++++- src/backend/src/ExtensionService.js | 27 ++++++-- src/backend/src/Kernel.js | 31 ++++++++- 3 files changed, 153 insertions(+), 6 deletions(-) diff --git a/src/backend/src/Extension.js b/src/backend/src/Extension.js index f71120d52..0a3d5fec4 100644 --- a/src/backend/src/Extension.js +++ b/src/backend/src/Extension.js @@ -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 diff --git a/src/backend/src/ExtensionService.js b/src/backend/src/ExtensionService.js index 15fad257b..9430ad33c 100644 --- a/src/backend/src/ExtensionService.js +++ b/src/backend/src/ExtensionService.js @@ -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 }) { diff --git a/src/backend/src/Kernel.js b/src/backend/src/Kernel.js index d49f70a12..81616e487 100644 --- a/src/backend/src/Kernel.js +++ b/src/backend/src/Kernel.js @@ -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);