@mulutime/plugin-sdk
Version:
SDK for developing MuluTime booking platform plugins
423 lines • 14.7 kB
JavaScript
"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