Commit compiled dist for git installs

This commit is contained in:
2026-04-18 20:11:29 +10:00
parent 234d269283
commit 15c9afd630
29 changed files with 1070 additions and 1 deletions

335
dist/PluginRegistry.js vendored Normal file
View File

@@ -0,0 +1,335 @@
import { MemoryStorage } from './interfaces.js';
import { generateThemeCSS } from './theme-css.js';
/**
* Central registry that manages all loaded plugins and their side-effects.
* Create a single instance and share it across your application.
*/
export class PluginRegistry {
constructor(options = {}) {
this.plugins = new Map();
this.commands = new Map();
this.beforeSendInterceptors = [];
this.receiveInterceptors = [];
this.renderers = new Map();
this.settingsSchemas = new Map();
this.pluginSettings = new Map();
this.matrixHandlers = new Map();
this.timers = new Map();
this.themes = new Map();
this.logs = [];
this.maxLogs = 1000;
this.storage = options.storage ?? new MemoryStorage();
this.onThemeRegistered = options.onThemeRegistered;
this.onThemeUnregistered = options.onThemeUnregistered;
}
// ---------------------------------------------------------------------------
// Plugin lifecycle
// ---------------------------------------------------------------------------
/** Register a plugin and its context. Called after the host has loaded the module. */
registerPlugin(id, plugin, context) {
if (this.plugins.has(id)) {
console.warn(`[PluginRegistry] Plugin ${id} is already registered`);
return;
}
this.plugins.set(id, { plugin, context });
this.timers.set(id, []);
}
/** Unregister a plugin, calling its onUnload hook and cleaning up all side-effects. */
async unregisterPlugin(id) {
const entry = this.plugins.get(id);
if (!entry)
return;
if (entry.plugin.onUnload) {
try {
await entry.plugin.onUnload();
}
catch (err) {
console.error(`[PluginRegistry] Error unloading plugin ${id}:`, err);
}
}
for (const [cmdName, cmd] of this.commands.entries()) {
if (cmd.pluginId === id)
this.commands.delete(cmdName);
}
this.beforeSendInterceptors = this.beforeSendInterceptors.filter(i => i.pluginId !== id);
this.receiveInterceptors = this.receiveInterceptors.filter(i => i.pluginId !== id);
for (const [type, renderer] of this.renderers.entries()) {
if (renderer.pluginId === id)
this.renderers.delete(type);
}
for (const [themeId, theme] of this.themes.entries()) {
if (theme.pluginId === id) {
this.onThemeUnregistered?.(themeId);
this.themes.delete(themeId);
}
}
const timerIds = this.timers.get(id) ?? [];
timerIds.forEach(timerId => clearInterval(timerId));
this.timers.delete(id);
for (const [eventType, handlers] of this.matrixHandlers.entries()) {
const filtered = handlers.filter(h => h.pluginId !== id);
if (filtered.length === 0) {
this.matrixHandlers.delete(eventType);
}
else {
this.matrixHandlers.set(eventType, filtered);
}
}
this.plugins.delete(id);
}
// ---------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------
/** Register a command contributed by a plugin. */
registerCommand(pluginId, command) {
if (this.commands.has(command.name)) {
console.warn(`[PluginRegistry] Command /${command.name} already exists`);
return;
}
this.commands.set(command.name, { pluginId, command });
}
/** Remove a registered command by name. */
unregisterCommand(name) {
this.commands.delete(name);
}
/** Execute a command by name, passing raw argument string. */
async executeCommand(name, rawArgs) {
const cmd = this.commands.get(name);
if (!cmd)
throw new Error(`Command /${name} not found`);
const args = this.parseCommandArgs(cmd.command, rawArgs);
const entry = this.plugins.get(cmd.pluginId);
if (!entry)
throw new Error(`Plugin ${cmd.pluginId} not loaded`);
return await cmd.command.run(args, entry.context);
}
parseCommandArgs(command, rawArgs) {
if (!command.args || command.args.length === 0)
return {};
const parts = rawArgs.trim().split(/\s+/);
const result = {};
command.args.forEach((arg, index) => {
const argName = typeof arg === 'string' ? arg : arg.name;
result[argName] = parts[index];
});
return result;
}
/** Return all registered commands. */
getCommands() {
return Array.from(this.commands.entries()).map(([name, data]) => ({ name, ...data }));
}
// ---------------------------------------------------------------------------
// Message interceptors
// ---------------------------------------------------------------------------
/** Add a before-send interceptor for a plugin. */
addBeforeSendInterceptor(pluginId, interceptor) {
this.beforeSendInterceptors.push({ pluginId, interceptor });
}
/** Add a receive interceptor for a plugin. */
addReceiveInterceptor(pluginId, interceptor) {
this.receiveInterceptors.push({ pluginId, interceptor });
}
/** Run a message through all before-send interceptors in registration order. */
async processBeforeSend(msg) {
let processed = { ...msg };
for (const { interceptor } of this.beforeSendInterceptors) {
try {
await interceptor(processed);
}
catch (err) {
console.error('[PluginRegistry] Error in beforeSend interceptor:', err);
}
}
return processed;
}
/** Run a message through all receive interceptors in registration order. */
async processReceive(msg) {
let processed = { ...msg };
for (const { interceptor } of this.receiveInterceptors) {
try {
await interceptor(processed);
}
catch (err) {
console.error('[PluginRegistry] Error in receive interceptor:', err);
}
}
return processed;
}
// ---------------------------------------------------------------------------
// Renderers
// ---------------------------------------------------------------------------
/** Register a custom renderer contributed by a plugin. */
registerRenderer(pluginId, type, renderer) {
this.renderers.set(type, { pluginId, renderer });
}
/** Remove a renderer by type. */
unregisterRenderer(type) {
this.renderers.delete(type);
}
/** Get the renderer for a given type, if any. */
getRenderer(type) {
return this.renderers.get(type)?.renderer;
}
// ---------------------------------------------------------------------------
// Settings
// ---------------------------------------------------------------------------
/** Define the settings schema for a plugin and load persisted values. */
defineSettings(pluginId, schema) {
this.settingsSchemas.set(pluginId, schema);
const stored = this.storage.getItem(`plugin:${pluginId}:settings`);
if (stored) {
try {
this.pluginSettings.set(pluginId, JSON.parse(stored));
}
catch (err) {
console.error(`[PluginRegistry] Failed to load settings for ${pluginId}:`, err);
}
}
else {
const defaults = {};
for (const [key, def] of Object.entries(schema)) {
if (def.default !== undefined)
defaults[key] = def.default;
}
this.pluginSettings.set(pluginId, defaults);
}
}
/** Get a plugin setting value. */
getPluginSetting(pluginId, key) {
return this.pluginSettings.get(pluginId)?.[key];
}
/** Set a plugin setting value and persist it. */
setPluginSetting(pluginId, key, value) {
const settings = this.pluginSettings.get(pluginId) ?? {};
settings[key] = value;
this.pluginSettings.set(pluginId, settings);
this.storage.setItem(`plugin:${pluginId}:settings`, JSON.stringify(settings));
}
/** Get the settings schema for a plugin. */
getPluginSettingsSchema(pluginId) {
return this.settingsSchemas.get(pluginId);
}
// ---------------------------------------------------------------------------
// Themes
// ---------------------------------------------------------------------------
/** Register a theme contributed by a plugin. */
registerTheme(pluginId, theme) {
const fullId = `plugin-${pluginId}-${theme.id}`;
if (this.themes.has(fullId)) {
console.warn(`[PluginRegistry] Theme ${fullId} already registered`);
return;
}
const css = generateThemeCSS(fullId, theme);
this.themes.set(fullId, { pluginId, theme: { ...theme, id: fullId } });
this.onThemeRegistered?.(fullId, fullId, css);
console.log(`[PluginRegistry] Theme registered: ${fullId}`);
}
/** Unregister a theme contributed by a plugin. */
unregisterTheme(pluginId, themeId) {
const fullId = `plugin-${pluginId}-${themeId}`;
if (this.themes.has(fullId)) {
this.onThemeUnregistered?.(fullId);
this.themes.delete(fullId);
}
}
/** Return all registered plugin themes. */
getPluginThemes() {
return Array.from(this.themes.values()).map(({ theme }) => ({
id: theme.id,
name: theme.name,
kind: theme.kind,
className: theme.id,
}));
}
// ---------------------------------------------------------------------------
// Event handlers (generic — wire to Matrix, socket.io, etc.)
// ---------------------------------------------------------------------------
/** Track an event handler registered by a plugin. */
addEventHandler(pluginId, eventType, handler) {
if (!this.matrixHandlers.has(eventType)) {
this.matrixHandlers.set(eventType, []);
}
this.matrixHandlers.get(eventType).push({ pluginId, handler });
}
/** Remove a previously tracked event handler. */
removeEventHandler(pluginId, eventType, handler) {
const handlers = this.matrixHandlers.get(eventType);
if (handlers) {
const filtered = handlers.filter(h => h.pluginId !== pluginId || h.handler !== handler);
this.matrixHandlers.set(eventType, filtered);
}
}
/** Get all tracked handlers for an event type. */
getEventHandlers(eventType) {
return this.matrixHandlers.get(eventType) ?? [];
}
// ---------------------------------------------------------------------------
// Timers
// ---------------------------------------------------------------------------
/** Track a timer ID for a plugin so it can be cleared on unload. */
addTimer(pluginId, timerId) {
const timers = this.timers.get(pluginId) ?? [];
timers.push(timerId);
this.timers.set(pluginId, timers);
}
/** Remove a timer ID from tracking (e.g. after manual clearInterval). */
removeTimer(pluginId, timerId) {
const timers = this.timers.get(pluginId) ?? [];
this.timers.set(pluginId, timers.filter(id => id !== timerId));
}
// ---------------------------------------------------------------------------
// Logging
// ---------------------------------------------------------------------------
/** Append a log entry from a plugin. */
addLog(pluginId, level, args) {
this.logs.push({ pluginId, level, timestamp: Date.now(), args });
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}
}
/** Return log entries, optionally filtered to a specific plugin. */
getLogs(pluginId) {
return pluginId ? this.logs.filter(l => l.pluginId === pluginId) : [...this.logs];
}
/** Clear log entries, optionally scoped to a specific plugin. */
clearLogs(pluginId) {
this.logs = pluginId ? this.logs.filter(l => l.pluginId !== pluginId) : [];
}
// ---------------------------------------------------------------------------
// Plugin exports (require())
// ---------------------------------------------------------------------------
/** Get the public exports of a loaded plugin. */
getPluginExports(pluginId) {
return this.plugins.get(pluginId)?.plugin.exports;
}
// ---------------------------------------------------------------------------
// Hot reload
// ---------------------------------------------------------------------------
/** Unload the current plugin instance and load a new one in-place. */
async reloadPlugin(pluginId, newPlugin, newContext) {
await this.unregisterPlugin(pluginId);
this.registerPlugin(pluginId, newPlugin, newContext);
await newPlugin.onLoad(newContext);
}
// ---------------------------------------------------------------------------
// Teardown
// ---------------------------------------------------------------------------
/** Unload all plugins and clear all registry state. */
clear() {
for (const pluginId of this.plugins.keys()) {
this.unregisterPlugin(pluginId).catch(err => {
console.error(`[PluginRegistry] Error clearing plugin ${pluginId}:`, err);
});
}
this.plugins.clear();
this.commands.clear();
this.beforeSendInterceptors = [];
this.receiveInterceptors = [];
this.renderers.clear();
this.settingsSchemas.clear();
this.matrixHandlers.clear();
this.timers.clear();
this.logs = [];
}
}
//# sourceMappingURL=PluginRegistry.js.map