feat: enhance plugin system with async support and lifecycle hooks & reverted PR #79 due to Node.js v24 stream compatibility issues

This commit is contained in:
Technomancer702
2026-02-05 01:42:40 -08:00
parent 6eafa871b5
commit 72e14dca57
6 changed files with 1177 additions and 63 deletions
+59 -17
View File
@@ -106,30 +106,67 @@ try {
console.warn('No services directory found or failed to read services:', e.message);
}
// Freeze services object to prevent plugins from mutating shared state
Object.freeze(services);
// Plugin loader: loads any .js file inside server/plugins and calls the
// exported function with (app, services).
try {
const pluginsDir = path.join(__dirname, 'plugins');
if (fs.existsSync(pluginsDir)) {
const pluginFiles = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.js'));
for (const file of pluginFiles) {
const pluginPath = path.join(pluginsDir, file);
try {
const plugin = require(pluginPath);
if (typeof plugin === 'function') {
plugin(app, services);
console.log(`Loaded plugin: ${file}`);
} else {
console.warn(`Plugin ${file} does not export a function, skipping.`);
// Supports both function exports and object exports with lifecycle hooks.
const loadedPlugins = [];
async function loadPlugins() {
try {
const pluginsDir = path.join(__dirname, 'plugins');
if (fs.existsSync(pluginsDir)) {
// Sort plugin files alphabetically for deterministic load order
const pluginFiles = fs.readdirSync(pluginsDir)
.filter(f => f.endsWith('.js'))
.sort();
for (const file of pluginFiles) {
const pluginPath = path.join(pluginsDir, file);
try {
const plugin = require(pluginPath);
// Support both function exports and object exports with lifecycle hooks
if (typeof plugin === 'function') {
// Direct function export (sync or async)
await plugin(app, services);
loadedPlugins.push({ name: file, plugin: null });
console.log(`✓ Loaded plugin: ${file}`);
} else if (plugin && typeof plugin.init === 'function') {
// Object export with init/shutdown lifecycle
await plugin.init(app, services);
loadedPlugins.push({ name: file, plugin });
console.log(`✓ Loaded plugin: ${file} (with lifecycle hooks)`);
} else {
console.warn(`⚠ Plugin ${file} does not export a function or object with init(), skipping.`);
}
} catch (err) {
console.error(`✗ Failed to load plugin ${file}:`, err);
}
}
}
} catch (err) {
console.warn('Plugin loader failed:', err.message);
}
}
// Graceful shutdown handler for plugins with shutdown hooks
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down plugins...');
for (const { name, plugin } of loadedPlugins) {
if (plugin && typeof plugin.shutdown === 'function') {
try {
await plugin.shutdown();
console.log(`✓ Shutdown plugin: ${name}`);
} catch (err) {
console.error(`Failed to load plugin ${file}:`, err);
console.error(`✗ Error shutting down plugin ${name}:`, err);
}
}
}
} catch (err) {
console.warn('Plugin loader failed:', err.message);
}
process.exit(0);
});
// API Routes
app.use('/api/auth', require('./routes/auth'));
@@ -164,6 +201,11 @@ app.use((err, req, res, next) => {
app.listen(PORT, async () => {
console.log(`NodeCast TV server running on http://localhost:${PORT}`);
// Load plugins
await loadPlugins().catch(err => {
console.error('Plugin initialization failed:', err);
});
// Trigger background sync with delay to allow server to settle
setTimeout(async () => {
await syncService.syncAll().catch(console.error);