@mulutime/plugin-sdk
Version:
SDK for developing MuluTime booking platform plugins
395 lines • 13.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LifecycleActionBuilder = exports.LifecycleActionHandler = void 0;
exports.OnInstall = OnInstall;
exports.OnUninstall = OnUninstall;
exports.OnUpdate = OnUpdate;
exports.OnEnable = OnEnable;
exports.OnDisable = OnDisable;
exports.ValidateConfig = ValidateConfig;
exports.createLifecycleActions = createLifecycleActions;
exports.extractLifecycleActions = extractLifecycleActions;
class LifecycleActionHandler {
constructor(options = {}) {
this.actions = new Map();
this.executions = [];
this.options = {
enableLogging: true,
timeout: 60000, // 1 minute timeout for lifecycle actions
maxRetries: 2,
retryDelay: 2000,
...options
};
}
/**
* Register lifecycle actions for a plugin
*/
register(pluginId, actions) {
this.actions.set(pluginId, actions);
}
/**
* Unregister lifecycle actions for a plugin
*/
unregister(pluginId) {
this.actions.delete(pluginId);
}
/**
* Execute the install lifecycle action
*/
async executeInstall(pluginId, context) {
const actions = this.actions.get(pluginId);
if (!actions?.onInstall) {
return;
}
await this.executeLifecycleAction('onInstall', pluginId, context, () => actions.onInstall(context));
}
/**
* Execute the uninstall lifecycle action
*/
async executeUninstall(pluginId, context) {
const actions = this.actions.get(pluginId);
if (!actions?.onUninstall) {
return;
}
await this.executeLifecycleAction('onUninstall', pluginId, context, () => actions.onUninstall(context));
}
/**
* Execute the update lifecycle action
*/
async executeUpdate(pluginId, context, oldVersion) {
const actions = this.actions.get(pluginId);
if (!actions?.onUpdate) {
return;
}
await this.executeLifecycleAction('onUpdate', pluginId, context, () => actions.onUpdate(context, oldVersion), { oldVersion });
}
/**
* Execute the enable lifecycle action
*/
async executeEnable(pluginId, context) {
const actions = this.actions.get(pluginId);
if (!actions?.onEnable) {
return;
}
await this.executeLifecycleAction('onEnable', pluginId, context, () => actions.onEnable(context));
}
/**
* Execute the disable lifecycle action
*/
async executeDisable(pluginId, context) {
const actions = this.actions.get(pluginId);
if (!actions?.onDisable) {
return;
}
await this.executeLifecycleAction('onDisable', pluginId, context, () => actions.onDisable(context));
}
/**
* Execute configuration validation
*/
async validateConfig(pluginId, config) {
const actions = this.actions.get(pluginId);
if (!actions?.validateConfig) {
return { valid: true };
}
try {
const result = await this.executeWithTimeout(() => actions.validateConfig(config), this.options.timeout);
return result;
}
catch (error) {
return {
valid: false,
errors: [error instanceof Error ? error.message : 'Configuration validation failed']
};
}
}
/**
* Execute a lifecycle action with error handling and retries
*/
async executeLifecycleAction(actionType, pluginId, context, executor, metadata) {
const startTime = Date.now();
const execution = {
id: `lifecycle-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
action: actionType,
timestamp: new Date(),
duration: 0,
success: false,
pluginId,
oldVersion: metadata?.oldVersion
};
let lastError = null;
for (let attempt = 0; attempt <= (this.options.maxRetries || 0); attempt++) {
try {
if (this.options.enableLogging) {
context.logger.info(`Executing ${actionType} lifecycle action`, {
pluginId,
attempt: attempt + 1,
executionId: execution.id
});
}
await this.executeWithTimeout(executor, this.options.timeout);
// Success
execution.success = true;
execution.duration = Date.now() - startTime;
if (this.options.enableLogging) {
context.logger.info(`${actionType} lifecycle action completed successfully`, {
pluginId,
duration: execution.duration,
executionId: execution.id
});
}
break;
}
catch (error) {
lastError = error;
execution.error = error instanceof Error ? error.message : String(error);
if (this.options.enableLogging) {
context.logger.warn(`${actionType} lifecycle action failed`, {
pluginId,
attempt: attempt + 1,
maxRetries: this.options.maxRetries,
error: execution.error,
executionId: execution.id
});
}
// If this is not the last attempt, wait before retrying
if (attempt < (this.options.maxRetries || 0)) {
await this.delay(this.options.retryDelay || 2000);
}
}
}
if (!execution.success) {
execution.duration = Date.now() - startTime;
if (this.options.enableLogging) {
context.logger.error(`${actionType} lifecycle action failed after all retries`, lastError || undefined, {
pluginId,
attempts: (this.options.maxRetries || 0) + 1,
duration: execution.duration,
executionId: execution.id
});
}
// Store failed execution
this.logExecution(execution);
throw lastError || new Error(`${actionType} lifecycle action failed`);
}
// Store successful execution
this.logExecution(execution);
}
/**
* Execute a function with timeout
*/
async executeWithTimeout(executor, timeout) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Lifecycle action timeout')), timeout);
});
return Promise.race([executor(), timeoutPromise]);
}
/**
* Delay utility for retries
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Log lifecycle execution
*/
logExecution(execution) {
this.executions.push(execution);
// Keep only last 500 executions
if (this.executions.length > 500) {
this.executions = this.executions.slice(-500);
}
}
/**
* Get execution history for a plugin
*/
getExecutionHistory(pluginId, limit = 50) {
let executions = this.executions;
if (pluginId) {
executions = executions.filter(e => e.pluginId === pluginId);
}
return executions
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
.slice(0, limit);
}
/**
* Get lifecycle statistics
*/
getStats() {
const totalExecutions = this.executions.length;
const successfulExecutions = this.executions.filter(e => e.success).length;
const failedExecutions = totalExecutions - successfulExecutions;
const averageExecutionTime = totalExecutions > 0
? this.executions.reduce((sum, e) => sum + e.duration, 0) / totalExecutions
: 0;
const executionsByAction = {
onInstall: 0,
onUninstall: 0,
onUpdate: 0,
onEnable: 0,
onDisable: 0,
validateConfig: 0
};
const executionsByPlugin = {};
this.executions.forEach(execution => {
executionsByAction[execution.action]++;
if (!executionsByPlugin[execution.pluginId]) {
executionsByPlugin[execution.pluginId] = 0;
}
executionsByPlugin[execution.pluginId]++;
});
return {
totalExecutions,
successfulExecutions,
failedExecutions,
averageExecutionTime,
executionsByAction,
executionsByPlugin
};
}
/**
* Check if a plugin has lifecycle actions registered
*/
hasActions(pluginId) {
return this.actions.has(pluginId);
}
/**
* Get the lifecycle actions for a plugin
*/
getActions(pluginId) {
return this.actions.get(pluginId);
}
/**
* Get all registered plugins
*/
getRegisteredPlugins() {
return Array.from(this.actions.keys());
}
/**
* Clear all registered actions and execution history
*/
clear() {
this.actions.clear();
this.executions = [];
}
/**
* Validate plugin state before lifecycle operations
*/
async validatePluginState(pluginId, actionType, context) {
const errors = [];
// Check if plugin is registered
if (!this.actions.has(pluginId)) {
errors.push(`Plugin ${pluginId} is not registered`);
}
// Check context validity
if (!context.pluginId) {
errors.push('Plugin context is missing plugin ID');
}
if (context.pluginId !== pluginId) {
errors.push('Plugin ID mismatch between context and request');
}
// Action-specific validations
switch (actionType) {
case 'onInstall':
// Check if plugin is already installed
break;
case 'onUninstall':
// Check if plugin is currently installed
break;
case 'onUpdate':
// Check if plugin is installed and version is different
break;
case 'onEnable':
// Check if plugin is installed but disabled
break;
case 'onDisable':
// Check if plugin is currently enabled
break;
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined
};
}
}
exports.LifecycleActionHandler = LifecycleActionHandler;
// =============================================================================
// LIFECYCLE ACTION BUILDER
// =============================================================================
class LifecycleActionBuilder {
constructor() {
this.actions = {};
}
static create() {
return new LifecycleActionBuilder();
}
onInstall(handler) {
this.actions.onInstall = handler;
return this;
}
onUninstall(handler) {
this.actions.onUninstall = handler;
return this;
}
onUpdate(handler) {
this.actions.onUpdate = handler;
return this;
}
onEnable(handler) {
this.actions.onEnable = handler;
return this;
}
onDisable(handler) {
this.actions.onDisable = handler;
return this;
}
validateConfig(handler) {
this.actions.validateConfig = handler;
return this;
}
build() {
return this.actions;
}
}
exports.LifecycleActionBuilder = LifecycleActionBuilder;
// =============================================================================
// LIFECYCLE ACTION DECORATORS
// =============================================================================
function OnInstall() {
return createLifecycleDecorator('onInstall');
}
function OnUninstall() {
return createLifecycleDecorator('onUninstall');
}
function OnUpdate() {
return createLifecycleDecorator('onUpdate');
}
function OnEnable() {
return createLifecycleDecorator('onEnable');
}
function OnDisable() {
return createLifecycleDecorator('onDisable');
}
function ValidateConfig() {
return createLifecycleDecorator('validateConfig');
}
function createLifecycleDecorator(actionType) {
return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
// Add metadata to the class
if (!target.constructor.__lifecycleActions) {
target.constructor.__lifecycleActions = {};
}
target.constructor.__lifecycleActions[actionType] = originalMethod;
return descriptor;
};
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function createLifecycleActions(handlers) {
return handlers;
}
function extractLifecycleActions(pluginClass) {
return pluginClass.__lifecycleActions || {};
}
//# sourceMappingURL=lifecycle-handler.js.map