/
h0tnanny
/
IotPlatform
Обзор
Документация
Войти
/
h0tnanny
/
IotPlatform
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
src/runtime/TriggerScheduler.ts
222 строки
8 KB
h0tnanny
Добавление функциональных требований
08 фев 2026, 23:02
08 фев 2026, 23:02
a8a23c5
Код
Авторство
О чём код?
import { Workflow, TriggerType } from '../entities/Workflow'; import { LogService } from '../services/LogService'; /** * TriggerScheduler - управляет автоматическим запуском workflow по триггерам * * Типы триггеров: * - manual: только ручной запуск (по кнопке или API) * - interval: периодический запуск с заданным интервалом * - cron: запуск по расписанию (cron-выражение) * - condition: запуск при выполнении условия (переменная достигла порога) */ export class TriggerScheduler { /** Последнее время запуска каждого workflow (для interval) */ private lastIntervalRun: Map<string, number> = new Map(); /** Последнее время запуска каждого cron workflow */ private lastCronRun: Map<string, number> = new Map(); /** Отслеживание: условие уже было true (чтобы не запускать повторно) */ private conditionWasTrue: Map<string, boolean> = new Map(); /** * Проверяет все триггеры и запускает workflow при необходимости * Вызывается из Main.tick() после syncData() */ checkTriggers(workflows: Workflow[]): void { const now = Date.now(); for (const workflow of workflows) { if (!workflow.isActivated) continue; if (workflow.executionState.isRunning) continue; switch (workflow.triggerType) { case 'interval': this.checkIntervalTrigger(workflow, now); break; case 'cron': this.checkCronTrigger(workflow, now); break; case 'condition': this.checkConditionTrigger(workflow); break; // 'manual' — не проверяем, запуск только руками } } } /** * Interval: запуск каждые N мс */ private checkIntervalTrigger(workflow: Workflow, now: number): void { const intervalMs = workflow.triggerConfig.intervalMs; if (!intervalMs || intervalMs < 100) return; const lastRun = this.lastIntervalRun.get(workflow.id) || 0; if (now - lastRun >= intervalMs) { this.lastIntervalRun.set(workflow.id, now); this.runWorkflow(workflow, 'interval'); } } /** * Cron: запуск по расписанию * Поддерживает 5-полевой формат: минута час день_месяца месяц день_недели */ private checkCronTrigger(workflow: Workflow, now: number): void { const cronExpression = workflow.triggerConfig.cronExpression; if (!cronExpression) return; // Не запускаем чаще чем раз в минуту const lastRun = this.lastCronRun.get(workflow.id) || 0; if (now - lastRun < 60000) return; if (this.matchesCron(cronExpression, new Date(now))) { this.lastCronRun.set(workflow.id, now); this.runWorkflow(workflow, 'cron'); } } /** * Condition: запуск когда переменная соответствует условию * Запускается однократно при переходе false→true (edge trigger) */ private checkConditionTrigger(workflow: Workflow): void { const { conditionVariable, conditionOperator, conditionValue } = workflow.triggerConfig; if (!conditionVariable || !conditionOperator) return; const field = workflow.variableList.get(conditionVariable); if (!field) return; const conditionMet = this.evaluateCondition( field.value, conditionOperator, conditionValue ); const wasPreviouslyTrue = this.conditionWasTrue.get(workflow.id) || false; if (conditionMet && !wasPreviouslyTrue) { // Edge trigger: false → true this.conditionWasTrue.set(workflow.id, true); this.runWorkflow(workflow, 'condition'); } else if (!conditionMet && wasPreviouslyTrue) { // Сбрасываем флаг, чтобы можно было сработать снова this.conditionWasTrue.set(workflow.id, false); } } /** * Запускает workflow */ private runWorkflow(workflow: Workflow, triggerSource: TriggerType): void { LogService.getInstance().info('workflow', `Триггер "${triggerSource}": запуск workflow "${workflow.name || workflow.id}"`, { resourceId: workflow.id, resourceType: 'workflow', details: { triggerType: triggerSource, triggerConfig: workflow.triggerConfig }, }); workflow.invoke().catch((error) => { console.error(`[TriggerScheduler] Ошибка выполнения workflow ${workflow.id}:`, error); workflow.executionState.isRunning = false; workflow.executionState.error = error.message || 'Неизвестная ошибка'; }); } /** * Проверяет, совпадает ли текущее время с cron-выражением * Формат: минута час день_месяца месяц день_недели * Поддерживает: *, числа, диапазоны (1-5), шаг (star/5), списки (1,3,5) */ private matchesCron(expression: string, date: Date): boolean { const parts = expression.trim().split(/\s+/); if (parts.length !== 5) return false; const [minExpr, hourExpr, domExpr, monthExpr, dowExpr] = parts; const minute = date.getMinutes(); const hour = date.getHours(); const dayOfMonth = date.getDate(); const month = date.getMonth() + 1; // 1-12 const dayOfWeek = date.getDay(); // 0=Sunday return ( this.matchCronField(minExpr, minute, 0, 59) && this.matchCronField(hourExpr, hour, 0, 23) && this.matchCronField(domExpr, dayOfMonth, 1, 31) && this.matchCronField(monthExpr, month, 1, 12) && this.matchCronField(dowExpr, dayOfWeek, 0, 6) ); } /** * Проверяет одно поле cron-выражения */ private matchCronField(expr: string, value: number, _min: number, _max: number): boolean { if (expr === '*') return true; // Шаг: */N if (expr.startsWith('*/')) { const step = parseInt(expr.slice(2), 10); if (isNaN(step) || step <= 0) return false; return value % step === 0; } // Список: 1,3,5 const parts = expr.split(','); for (const part of parts) { // Диапазон: 1-5 if (part.includes('-')) { const [startStr, endStr] = part.split('-'); const start = parseInt(startStr, 10); const end = parseInt(endStr, 10); if (!isNaN(start) && !isNaN(end) && value >= start && value <= end) { return true; } } else { const num = parseInt(part, 10); if (!isNaN(num) && num === value) { return true; } } } return false; } /** * Вычисляет условие сравнения */ private evaluateCondition( fieldValue: unknown, operator: string, compareValue: unknown ): boolean { switch (operator) { case '==': return fieldValue == compareValue; case '!=': return fieldValue != compareValue; case '>': return Number(fieldValue) > Number(compareValue); case '<': return Number(fieldValue) < Number(compareValue); case '>=': return Number(fieldValue) >= Number(compareValue); case '<=': return Number(fieldValue) <= Number(compareValue); default: return false; } } /** * Очищает состояние для удалённого workflow */ cleanup(workflowId: string): void { this.lastIntervalRun.delete(workflowId); this.lastCronRun.delete(workflowId); this.conditionWasTrue.delete(workflowId); } }