UNPKG

@mulutime/plugin-sdk

Version:

SDK for developing MuluTime booking platform plugins

423 lines 14.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ScheduledActionBuilder = exports.ScheduledActionHandler = void 0; exports.Scheduled = Scheduled; exports.createScheduledAction = createScheduledAction; exports.extractScheduledActions = extractScheduledActions; class ScheduledActionHandler { constructor(options = {}) { this.actions = new Map(); this.executions = []; this.timers = new Map(); this.options = { timezone: 'UTC', maxRetries: 3, retryDelay: 5000, timeout: 60000, ...options }; } /** * Register a scheduled action */ register(action) { this.actions.set(action.name, action); if (action.enabled) { this.scheduleAction(action); } } /** * Register multiple scheduled actions */ registerMany(actions) { actions.forEach(action => this.register(action)); } /** * Unregister a scheduled action */ unregister(actionName) { // Clear existing timer const timer = this.timers.get(actionName); if (timer) { clearTimeout(timer); this.timers.delete(actionName); } this.actions.delete(actionName); } /** * Enable a scheduled action */ enable(actionName) { const action = this.actions.get(actionName); if (action) { action.enabled = true; this.scheduleAction(action); } } /** * Disable a scheduled action */ disable(actionName) { const action = this.actions.get(actionName); if (action) { action.enabled = false; // Clear existing timer const timer = this.timers.get(actionName); if (timer) { clearTimeout(timer); this.timers.delete(actionName); } } } /** * Execute a scheduled action immediately */ async executeNow(actionName, context) { const action = this.actions.get(actionName); if (!action) { throw new Error(`Scheduled action '${actionName}' not found`); } await this.executeAction(action, context); } /** * Schedule an action based on its schedule type */ scheduleAction(action, context) { // Clear existing timer const existingTimer = this.timers.get(action.name); if (existingTimer) { clearTimeout(existingTimer); } const nextExecution = this.calculateNextExecution(action); if (!nextExecution) { return; } const delay = nextExecution.getTime() - Date.now(); if (delay > 0) { const timer = setTimeout(async () => { if (context) { await this.executeAction(action, context); // Reschedule for next execution this.scheduleAction(action, context); } }, delay); this.timers.set(action.name, timer); } } /** * Calculate the next execution time for an action */ calculateNextExecution(action) { const now = new Date(); switch (action.schedule) { case 'HOURLY': return new Date(now.getTime() + 60 * 60 * 1000); case 'DAILY': if (action.time) { const [hours, minutes = 0] = action.time.split(':').map(Number); const next = new Date(); next.setHours(hours, minutes, 0, 0); // If time has passed today, schedule for tomorrow if (next <= now) { next.setDate(next.getDate() + 1); } return next; } break; case 'WEEKLY': if (action.time) { const parts = action.time.split(' '); if (parts.length === 2) { const dayName = parts[0].toUpperCase(); const [hours, minutes = 0] = parts[1].split(':').map(Number); const dayMap = { 'SUN': 0, 'MON': 1, 'TUE': 2, 'WED': 3, 'THU': 4, 'FRI': 5, 'SAT': 6 }; const targetDay = dayMap[dayName]; if (targetDay !== undefined) { const next = new Date(); const currentDay = next.getDay(); let daysUntilTarget = targetDay - currentDay; if (daysUntilTarget <= 0) { daysUntilTarget += 7; } next.setDate(next.getDate() + daysUntilTarget); next.setHours(hours, minutes, 0, 0); return next; } } } break; case 'MONTHLY': const nextMonth = new Date(now); nextMonth.setMonth(nextMonth.getMonth() + 1); nextMonth.setDate(1); if (action.time) { const [hours, minutes = 0] = action.time.split(':').map(Number); nextMonth.setHours(hours, minutes, 0, 0); } else { nextMonth.setHours(0, 0, 0, 0); } return nextMonth; case 'CUSTOM': if (action.cronExpression) { // Basic cron parsing - in a real implementation, use a cron library return this.parseCronExpression(action.cronExpression, now); } break; } return null; } /** * Basic cron expression parser (simplified) */ parseCronExpression(cron, from) { // This is a simplified implementation // In production, use a proper cron library like 'node-cron' or 'cron-parser' const parts = cron.trim().split(/\s+/); if (parts.length !== 5) { return null; } // For now, just return next hour as a placeholder return new Date(from.getTime() + 60 * 60 * 1000); } /** * Execute a scheduled action with retry logic */ async executeAction(action, context) { const startTime = Date.now(); const execution = { id: `${action.name}-${Date.now()}`, actionName: action.name, scheduledTime: new Date(), // Should be the actual scheduled time actualTime: new Date(), success: false, duration: 0 }; let lastError = null; for (let attempt = 0; attempt <= (this.options.maxRetries || 0); attempt++) { try { // Add timeout wrapper const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Scheduled action timeout')), this.options.timeout); }); const actionPromise = action.handler(context); await Promise.race([actionPromise, timeoutPromise]); // Success execution.success = true; execution.duration = Date.now() - startTime; context.logger.info(`Scheduled action executed successfully`, { actionName: action.name, executionId: execution.id, duration: execution.duration, attempt: attempt + 1 }); break; } catch (error) { lastError = error; execution.error = error instanceof Error ? error.message : String(error); context.logger.warn(`Scheduled action failed`, { actionName: action.name, executionId: execution.id, attempt: attempt + 1, maxRetries: this.options.maxRetries, error: execution.error }); // If this is not the last attempt, wait before retrying if (attempt < (this.options.maxRetries || 0)) { await this.delay(this.options.retryDelay || 5000); } } } if (!execution.success) { execution.duration = Date.now() - startTime; context.logger.error(`Scheduled action failed after all retries`, lastError || undefined, { actionName: action.name, executionId: execution.id, attempts: (this.options.maxRetries || 0) + 1, duration: execution.duration }); } // Store execution record this.executions.push(execution); // Keep only last 100 executions per action const actionExecutions = this.executions.filter(e => e.actionName === action.name); if (actionExecutions.length > 100) { const toRemove = actionExecutions.slice(0, actionExecutions.length - 100); this.executions = this.executions.filter(e => !toRemove.includes(e)); } } /** * Delay utility for retries */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Get all registered actions */ getRegisteredActions() { return Array.from(this.actions.values()); } /** * Get execution history */ getExecutionHistory(actionName, limit = 50) { let executions = this.executions; if (actionName) { executions = executions.filter(e => e.actionName === actionName); } return executions .sort((a, b) => b.actualTime.getTime() - a.actualTime.getTime()) .slice(0, limit); } /** * Get statistics about scheduled actions */ getStats() { const totalActions = this.actions.size; const enabledActions = Array.from(this.actions.values()).filter(a => a.enabled).length; 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; return { totalActions, enabledActions, totalExecutions, successfulExecutions, failedExecutions, averageExecutionTime }; } /** * Clear all scheduled actions and timers */ clear() { // Clear all timers for (const timer of this.timers.values()) { clearTimeout(timer); } this.timers.clear(); this.actions.clear(); this.executions = []; } /** * Start all enabled scheduled actions */ start(context) { for (const action of this.actions.values()) { if (action.enabled) { this.scheduleAction(action, context); } } } /** * Stop all scheduled actions */ stop() { for (const timer of this.timers.values()) { clearTimeout(timer); } this.timers.clear(); } } exports.ScheduledActionHandler = ScheduledActionHandler; // ============================================================================= // SCHEDULED ACTION BUILDER // ============================================================================= class ScheduledActionBuilder { constructor() { this.action = {}; } static create() { return new ScheduledActionBuilder(); } name(name) { this.action.name = name; return this; } schedule(schedule) { this.action.schedule = schedule; return this; } time(time) { this.action.time = time; return this; } cronExpression(cron) { this.action.cronExpression = cron; return this; } handler(handler) { this.action.handler = handler; return this; } enabled(enabled = true) { this.action.enabled = enabled; return this; } timezone(timezone) { this.action.timezone = timezone; return this; } build() { if (!this.action.name || !this.action.schedule || !this.action.handler) { throw new Error('Name, schedule, and handler are required'); } return { name: this.action.name, schedule: this.action.schedule, time: this.action.time, cronExpression: this.action.cronExpression, handler: this.action.handler, enabled: this.action.enabled ?? true, timezone: this.action.timezone }; } } exports.ScheduledActionBuilder = ScheduledActionBuilder; // ============================================================================= // SCHEDULED ACTION DECORATORS // ============================================================================= function Scheduled(schedule, options) { return function (target, propertyKey, descriptor) { const originalMethod = descriptor.value; // Add metadata to the method if (!target.constructor.__scheduledActions) { target.constructor.__scheduledActions = []; } target.constructor.__scheduledActions.push({ name: propertyKey, schedule, time: options?.time, cronExpression: options?.cronExpression, handler: originalMethod, enabled: true, timezone: options?.timezone }); return descriptor; }; } // ============================================================================= // HELPER FUNCTIONS // ============================================================================= function createScheduledAction(name, schedule, handler, options) { return { name, schedule, time: options?.time, cronExpression: options?.cronExpression, handler, enabled: options?.enabled ?? true, timezone: options?.timezone }; } function extractScheduledActions(pluginClass) { return pluginClass.__scheduledActions || []; } //# sourceMappingURL=scheduled-handler.js.map