hyper-scheduler 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +21 -0
- package/.eslintrc.cjs +26 -0
- package/GEMINI.md +1 -0
- package/README.md +38 -0
- package/docs/.vitepress/config.ts +52 -0
- package/docs/README.md +120 -0
- package/docs/api/devtools.md +232 -0
- package/docs/api/index.md +178 -0
- package/docs/api/scheduler.md +322 -0
- package/docs/api/task.md +439 -0
- package/docs/api/types.md +365 -0
- package/docs/examples/index.md +295 -0
- package/docs/guide/best-practices.md +436 -0
- package/docs/guide/core-concepts.md +363 -0
- package/docs/guide/getting-started.md +138 -0
- package/docs/index.md +33 -0
- package/docs/public/logo.svg +54 -0
- package/examples/browser/index.html +354 -0
- package/examples/node/simple.js +36 -0
- package/examples/react-demo/index.html +12 -0
- package/examples/react-demo/package.json +23 -0
- package/examples/react-demo/src/App.css +212 -0
- package/examples/react-demo/src/App.jsx +160 -0
- package/examples/react-demo/src/main.jsx +9 -0
- package/examples/react-demo/vite.config.ts +12 -0
- package/examples/react-demo/yarn.lock +752 -0
- package/examples/vue-demo/index.html +12 -0
- package/examples/vue-demo/package.json +21 -0
- package/examples/vue-demo/src/App.vue +373 -0
- package/examples/vue-demo/src/main.ts +4 -0
- package/examples/vue-demo/vite.config.ts +13 -0
- package/examples/vue-demo/yarn.lock +375 -0
- package/package.json +51 -0
- package/src/constants.ts +18 -0
- package/src/core/retry-strategy.ts +28 -0
- package/src/core/scheduler.ts +601 -0
- package/src/core/task-registry.ts +58 -0
- package/src/index.ts +74 -0
- package/src/platform/browser/browser-timer.ts +66 -0
- package/src/platform/browser/main-thread-timer.ts +16 -0
- package/src/platform/browser/worker.ts +31 -0
- package/src/platform/node/debug-cli.ts +19 -0
- package/src/platform/node/node-timer.ts +15 -0
- package/src/platform/timer-strategy.ts +19 -0
- package/src/plugins/dev-tools.ts +101 -0
- package/src/types.ts +115 -0
- package/src/ui/components/devtools.ts +525 -0
- package/src/ui/components/floating-trigger.ts +102 -0
- package/src/ui/components/icons.ts +16 -0
- package/src/ui/components/resizer.ts +129 -0
- package/src/ui/components/task-detail.ts +228 -0
- package/src/ui/components/task-header.ts +319 -0
- package/src/ui/components/task-list.ts +416 -0
- package/src/ui/components/timeline.ts +364 -0
- package/src/ui/debug-panel.ts +56 -0
- package/src/ui/i18n/en.ts +76 -0
- package/src/ui/i18n/index.ts +42 -0
- package/src/ui/i18n/zh.ts +76 -0
- package/src/ui/store/dev-tools-store.ts +191 -0
- package/src/ui/styles/theme.css.ts +56 -0
- package/src/ui/styles.ts +43 -0
- package/src/utils/cron-lite.ts +221 -0
- package/src/utils/cron.ts +20 -0
- package/src/utils/id.ts +10 -0
- package/src/utils/schedule.ts +93 -0
- package/src/vite-env.d.ts +1 -0
- package/stats.html +4949 -0
- package/tests/integration/Debug.test.ts +58 -0
- package/tests/unit/Plugin.test.ts +16 -0
- package/tests/unit/RetryStrategy.test.ts +21 -0
- package/tests/unit/Scheduler.test.ts +38 -0
- package/tests/unit/schedule.test.ts +70 -0
- package/tests/unit/ui/DevToolsStore.test.ts +67 -0
- package/tsconfig.json +28 -0
- package/vite.config.ts +51 -0
- package/vitest.config.ts +24 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { SchedulerConfig, Task, TaskDefinition, ExecutionRecord } from '../types';
|
|
2
|
+
import { TaskStatus, SchedulerEvents } from '../constants';
|
|
3
|
+
import { TaskRegistry } from './task-registry';
|
|
4
|
+
import { TimerStrategy } from '../platform/timer-strategy';
|
|
5
|
+
import { validateId } from '../utils/id';
|
|
6
|
+
import { parseSchedule, getNextRun as getNextScheduleRun } from '../utils/schedule'; // 替换为新的 schedule 工具
|
|
7
|
+
import { RetryStrategy } from './retry-strategy';
|
|
8
|
+
|
|
9
|
+
export type TimerStrategyFactory = (driver: 'worker' | 'main') => TimerStrategy;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 核心调度器类。
|
|
13
|
+
* 负责管理任务生命周期、时间循环和任务执行。
|
|
14
|
+
*/
|
|
15
|
+
export class Scheduler {
|
|
16
|
+
private registry: TaskRegistry;
|
|
17
|
+
private config: SchedulerConfig;
|
|
18
|
+
private defaultTimerStrategy: TimerStrategy;
|
|
19
|
+
private timerStrategyFactory?: TimerStrategyFactory;
|
|
20
|
+
private taskTimerStrategies: Map<string, TimerStrategy>; // Task ID -> TimerStrategy
|
|
21
|
+
private running: boolean;
|
|
22
|
+
private timers: Map<string, any>; // Task ID -> Timer Handle
|
|
23
|
+
private listeners: ((tasks: Task[]) => void)[];
|
|
24
|
+
private eventListeners: Map<string, Set<(payload: any) => void>>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 创建一个新的调度器实例。
|
|
28
|
+
* @param timerStrategy 默认计时策略(NodeTimer 或 BrowserTimer)
|
|
29
|
+
* @param config 调度器配置
|
|
30
|
+
* @param timerStrategyFactory 可选的定时器策略工厂,用于创建任务级别的定时器
|
|
31
|
+
*/
|
|
32
|
+
constructor(
|
|
33
|
+
timerStrategy: TimerStrategy,
|
|
34
|
+
config: SchedulerConfig = {},
|
|
35
|
+
timerStrategyFactory?: TimerStrategyFactory
|
|
36
|
+
) {
|
|
37
|
+
this.registry = new TaskRegistry();
|
|
38
|
+
this.defaultTimerStrategy = timerStrategy;
|
|
39
|
+
this.timerStrategyFactory = timerStrategyFactory;
|
|
40
|
+
this.taskTimerStrategies = new Map();
|
|
41
|
+
this.config = {
|
|
42
|
+
debug: false,
|
|
43
|
+
maxHistory: 50,
|
|
44
|
+
...config,
|
|
45
|
+
};
|
|
46
|
+
this.running = false;
|
|
47
|
+
this.timers = new Map();
|
|
48
|
+
this.listeners = [];
|
|
49
|
+
this.eventListeners = new Map();
|
|
50
|
+
|
|
51
|
+
// Initialize plugins
|
|
52
|
+
if (this.config.plugins && Array.isArray(this.config.plugins)) {
|
|
53
|
+
this.config.plugins.forEach(plugin => {
|
|
54
|
+
try {
|
|
55
|
+
this.log(`Initializing plugin: ${plugin.name}`);
|
|
56
|
+
plugin.init(this);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.warn(`[HyperScheduler] Failed to initialize plugin ${plugin.name}:`, err);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 订阅任务列表变更事件。
|
|
66
|
+
* 当任务创建、状态改变或删除时触发。
|
|
67
|
+
* @param listener 回调函数
|
|
68
|
+
* @returns 取消订阅的函数
|
|
69
|
+
*/
|
|
70
|
+
subscribe(listener: (tasks: Task[]) => void): () => void {
|
|
71
|
+
this.listeners.push(listener);
|
|
72
|
+
return () => {
|
|
73
|
+
this.listeners = this.listeners.filter(l => l !== listener);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 订阅特定事件
|
|
79
|
+
* @param event 事件名称
|
|
80
|
+
* @param handler 事件处理函数
|
|
81
|
+
* @returns 取消订阅的函数
|
|
82
|
+
*/
|
|
83
|
+
on(event: string, handler: (payload: any) => void): () => void {
|
|
84
|
+
if (!this.eventListeners.has(event)) {
|
|
85
|
+
this.eventListeners.set(event, new Set());
|
|
86
|
+
}
|
|
87
|
+
this.eventListeners.get(event)!.add(handler);
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
this.eventListeners.get(event)?.delete(handler);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private emit(event: string, payload: any): void {
|
|
95
|
+
const handlers = this.eventListeners.get(event);
|
|
96
|
+
if (handlers) {
|
|
97
|
+
handlers.forEach(handler => handler(payload));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 获取任务的定时器策略
|
|
103
|
+
* 优先使用任务级别配置,其次使用全局配置,最后使用默认策略
|
|
104
|
+
*/
|
|
105
|
+
private getTimerStrategy(task: Task): TimerStrategy {
|
|
106
|
+
const taskDriver = task.options?.driver;
|
|
107
|
+
const globalDriver = this.config.driver;
|
|
108
|
+
const driver = taskDriver || globalDriver;
|
|
109
|
+
|
|
110
|
+
// 如果没有指定 driver 或没有工厂函数,使用默认策略
|
|
111
|
+
if (!driver || !this.timerStrategyFactory) {
|
|
112
|
+
return this.defaultTimerStrategy;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 检查是否已经为该任务创建了策略
|
|
116
|
+
const cacheKey = `${task.id}_${driver}`;
|
|
117
|
+
if (this.taskTimerStrategies.has(cacheKey)) {
|
|
118
|
+
return this.taskTimerStrategies.get(cacheKey)!;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 创建新的策略并缓存
|
|
122
|
+
const strategy = this.timerStrategyFactory(driver);
|
|
123
|
+
this.taskTimerStrategies.set(cacheKey, strategy);
|
|
124
|
+
return strategy;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private notify(): void {
|
|
128
|
+
if (this.listeners.length > 0) {
|
|
129
|
+
const tasks = this.registry.getAllTasks();
|
|
130
|
+
this.listeners.forEach(l => l(tasks));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 创建并注册一个新任务。
|
|
136
|
+
* @param definition 任务定义
|
|
137
|
+
*/
|
|
138
|
+
createTask(definition: TaskDefinition): void {
|
|
139
|
+
validateId(definition.id);
|
|
140
|
+
parseSchedule(definition.schedule); // 使用新的解析函数进行验证
|
|
141
|
+
|
|
142
|
+
const task: Task = {
|
|
143
|
+
...definition,
|
|
144
|
+
tags: definition.tags || [],
|
|
145
|
+
// 新创建的任务默认是 stopped 状态,需要手动启动或等调度器启动
|
|
146
|
+
status: TaskStatus.STOPPED,
|
|
147
|
+
history: [],
|
|
148
|
+
executionCount: 0,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
this.registry.addTask(task);
|
|
152
|
+
this.log(`Task created: ${task.id}`);
|
|
153
|
+
this.emit(SchedulerEvents.TASK_REGISTERED, { taskId: task.id, task });
|
|
154
|
+
this.notify();
|
|
155
|
+
|
|
156
|
+
// 如果调度器已经在运行,自动启动新任务
|
|
157
|
+
if (this.running) {
|
|
158
|
+
task.status = TaskStatus.IDLE;
|
|
159
|
+
this.scheduleTask(task);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 删除指定 ID 的任务。
|
|
165
|
+
* @param id 任务 ID
|
|
166
|
+
* @returns 如果删除成功返回 true,否则返回 false
|
|
167
|
+
*/
|
|
168
|
+
deleteTask(id: string): boolean {
|
|
169
|
+
this.stopTask(id);
|
|
170
|
+
const deleted = this.registry.deleteTask(id);
|
|
171
|
+
if (deleted) {
|
|
172
|
+
this.log(`Task deleted: ${id}`);
|
|
173
|
+
this.emit(SchedulerEvents.TASK_REMOVED, { taskId: id });
|
|
174
|
+
this.notify();
|
|
175
|
+
}
|
|
176
|
+
return deleted;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 手动启动一个任务(将其置为 idle 状态并重新加入调度)。
|
|
181
|
+
* 即使调度器未启动,也可以单独启动某个任务。
|
|
182
|
+
* @param id 任务 ID
|
|
183
|
+
*/
|
|
184
|
+
startTask(id: string): void {
|
|
185
|
+
const task = this.registry.getTask(id);
|
|
186
|
+
if (!task) {
|
|
187
|
+
throw new Error(`Task not found: ${id}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (task.status === TaskStatus.RUNNING) {
|
|
191
|
+
return; // Already running
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Cancel existing timer if any
|
|
195
|
+
const existingHandle = this.timers.get(id);
|
|
196
|
+
if (existingHandle) {
|
|
197
|
+
this.getTimerStrategy(task).cancel(existingHandle);
|
|
198
|
+
this.timers.delete(id);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
task.status = TaskStatus.IDLE; // Reset status if it was stopped or error
|
|
202
|
+
this.log(`Starting task: ${id}`);
|
|
203
|
+
this.emit(SchedulerEvents.TASK_STARTED, { taskId: id, task });
|
|
204
|
+
this.notify();
|
|
205
|
+
|
|
206
|
+
// Schedule task even if scheduler is not running (individual task control)
|
|
207
|
+
this.scheduleTaskForce(task);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 强制调度任务,即使调度器未启动
|
|
212
|
+
*/
|
|
213
|
+
private scheduleTaskForce(task: Task): void {
|
|
214
|
+
try {
|
|
215
|
+
const nextRun = getNextScheduleRun(task.schedule, {
|
|
216
|
+
timezone: task.options?.timezone || this.config.timezone,
|
|
217
|
+
lastRun: task.lastRun,
|
|
218
|
+
});
|
|
219
|
+
task.nextRun = nextRun.getTime();
|
|
220
|
+
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const delay = Math.max(0, nextRun.getTime() - now);
|
|
223
|
+
|
|
224
|
+
this.log(`Scheduling task ${task.id} for ${nextRun.toISOString()} (in ${delay}ms)`);
|
|
225
|
+
|
|
226
|
+
const timerStrategy = this.getTimerStrategy(task);
|
|
227
|
+
const handle = timerStrategy.schedule(() => {
|
|
228
|
+
this.executeTaskIndividual(task);
|
|
229
|
+
}, delay);
|
|
230
|
+
|
|
231
|
+
this.timers.set(task.id, handle);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
this.log(`Error scheduling task ${task.id}: ${err}`);
|
|
234
|
+
task.status = TaskStatus.ERROR;
|
|
235
|
+
this.notify();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 执行单个任务(用于独立启动的任务)
|
|
241
|
+
*/
|
|
242
|
+
private async executeTaskIndividual(task: Task, attempt: number = 0): Promise<void> {
|
|
243
|
+
this.timers.delete(task.id);
|
|
244
|
+
|
|
245
|
+
// Check if task was stopped
|
|
246
|
+
if (task.status === TaskStatus.STOPPED) return;
|
|
247
|
+
|
|
248
|
+
task.status = TaskStatus.RUNNING;
|
|
249
|
+
task.lastRun = Date.now();
|
|
250
|
+
task.executionCount = (task.executionCount || 0) + 1;
|
|
251
|
+
const startTime = Date.now();
|
|
252
|
+
|
|
253
|
+
this.log(`Executing task: ${task.id} (Attempt ${attempt})`);
|
|
254
|
+
this.emit(SchedulerEvents.TASK_STARTED, { taskId: task.id, task });
|
|
255
|
+
this.notify();
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
await task.handler();
|
|
259
|
+
|
|
260
|
+
const duration = Date.now() - startTime;
|
|
261
|
+
this.recordHistory(task, {
|
|
262
|
+
timestamp: startTime,
|
|
263
|
+
duration,
|
|
264
|
+
success: true,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
task.status = TaskStatus.IDLE;
|
|
268
|
+
this.log(`Task execution success: ${task.id}`);
|
|
269
|
+
this.emit(SchedulerEvents.TASK_COMPLETED, { taskId: task.id, task, duration });
|
|
270
|
+
this.notify();
|
|
271
|
+
|
|
272
|
+
// Schedule next run
|
|
273
|
+
this.scheduleTaskForce(task);
|
|
274
|
+
|
|
275
|
+
} catch (err: any) {
|
|
276
|
+
const duration = Date.now() - startTime;
|
|
277
|
+
this.recordHistory(task, {
|
|
278
|
+
timestamp: startTime,
|
|
279
|
+
duration,
|
|
280
|
+
success: false,
|
|
281
|
+
error: err.message,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
this.log(`Task execution failed: ${task.id} - ${err.message}`);
|
|
285
|
+
this.emit(SchedulerEvents.TASK_FAILED, { taskId: task.id, task, error: err.message, duration });
|
|
286
|
+
|
|
287
|
+
// 调用任务的 onError 回调
|
|
288
|
+
if (task.options?.onError) {
|
|
289
|
+
try {
|
|
290
|
+
task.options.onError(err, task.id);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
this.log(`onError callback failed: ${e}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Retry logic
|
|
297
|
+
const retryDelay = RetryStrategy.getDelay(attempt, task.options?.retry);
|
|
298
|
+
if (retryDelay >= 0) {
|
|
299
|
+
this.log(`Retrying task ${task.id} in ${retryDelay}ms (Attempt ${attempt + 1})`);
|
|
300
|
+
const timerStrategy = this.getTimerStrategy(task);
|
|
301
|
+
const handle = timerStrategy.schedule(() => {
|
|
302
|
+
this.executeTaskIndividual(task, attempt + 1);
|
|
303
|
+
}, retryDelay);
|
|
304
|
+
this.timers.set(task.id, handle);
|
|
305
|
+
task.status = TaskStatus.ERROR;
|
|
306
|
+
this.notify();
|
|
307
|
+
} else {
|
|
308
|
+
task.status = TaskStatus.ERROR;
|
|
309
|
+
this.notify();
|
|
310
|
+
// Schedule next regular run
|
|
311
|
+
this.scheduleTaskForce(task);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 停止一个任务(取消当前的定时器并标记为 stopped)。
|
|
318
|
+
* @param id 任务 ID
|
|
319
|
+
*/
|
|
320
|
+
stopTask(id: string): void {
|
|
321
|
+
const task = this.registry.getTask(id);
|
|
322
|
+
const handle = this.timers.get(id);
|
|
323
|
+
if (handle && task) {
|
|
324
|
+
this.getTimerStrategy(task).cancel(handle);
|
|
325
|
+
this.timers.delete(id);
|
|
326
|
+
} else if (handle) {
|
|
327
|
+
// Fallback to default strategy if task not found
|
|
328
|
+
this.defaultTimerStrategy.cancel(handle);
|
|
329
|
+
this.timers.delete(id);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (task) {
|
|
333
|
+
task.status = TaskStatus.STOPPED;
|
|
334
|
+
this.log(`Task stopped: ${id}`);
|
|
335
|
+
this.emit(SchedulerEvents.TASK_STOPPED, { taskId: id, task });
|
|
336
|
+
this.notify();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 获取任务信息。
|
|
342
|
+
* @param id 任务 ID
|
|
343
|
+
*/
|
|
344
|
+
getTask(id: string): Task | undefined {
|
|
345
|
+
return this.registry.getTask(id);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* 获取所有任务。
|
|
350
|
+
*/
|
|
351
|
+
getAllTasks(): Task[] {
|
|
352
|
+
return this.registry.getAllTasks();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 获取调度器运行状态。
|
|
357
|
+
*/
|
|
358
|
+
isRunning(): boolean {
|
|
359
|
+
return this.running;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* 获取任务的实际驱动方式。
|
|
364
|
+
* 优先使用任务级配置,其次使用全局配置,默认为 'worker'。
|
|
365
|
+
*/
|
|
366
|
+
getTaskDriver(id: string): 'worker' | 'main' {
|
|
367
|
+
const task = this.registry.getTask(id);
|
|
368
|
+
if (!task) return this.config.driver || 'worker';
|
|
369
|
+
return task.options?.driver || this.config.driver || 'worker';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 获取全局驱动配置。
|
|
374
|
+
*/
|
|
375
|
+
getGlobalDriver(): 'worker' | 'main' {
|
|
376
|
+
return this.config.driver || 'worker';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 手动触发任务执行(忽略调度器状态,立即执行一次)。
|
|
381
|
+
* 执行完成后恢复到之前的状态。
|
|
382
|
+
* @param id 任务 ID
|
|
383
|
+
*/
|
|
384
|
+
async triggerTask(id: string): Promise<void> {
|
|
385
|
+
const task = this.getTask(id);
|
|
386
|
+
if (!task) return;
|
|
387
|
+
if (task.status === TaskStatus.RUNNING) return;
|
|
388
|
+
|
|
389
|
+
const previousStatus = task.status;
|
|
390
|
+
|
|
391
|
+
task.status = TaskStatus.RUNNING;
|
|
392
|
+
task.lastRun = Date.now();
|
|
393
|
+
task.executionCount = (task.executionCount || 0) + 1;
|
|
394
|
+
const startTime = Date.now();
|
|
395
|
+
|
|
396
|
+
this.log(`Triggering task: ${task.id}`);
|
|
397
|
+
this.emit(SchedulerEvents.TASK_STARTED, { taskId: task.id, task });
|
|
398
|
+
this.notify();
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
await task.handler();
|
|
402
|
+
|
|
403
|
+
const duration = Date.now() - startTime;
|
|
404
|
+
this.recordHistory(task, {
|
|
405
|
+
timestamp: startTime,
|
|
406
|
+
duration,
|
|
407
|
+
success: true,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// 恢复到之前的状态
|
|
411
|
+
task.status = previousStatus;
|
|
412
|
+
this.log(`Task trigger success: ${task.id}`);
|
|
413
|
+
this.emit(SchedulerEvents.TASK_COMPLETED, { taskId: task.id, task, duration });
|
|
414
|
+
this.notify();
|
|
415
|
+
|
|
416
|
+
} catch (err: any) {
|
|
417
|
+
const duration = Date.now() - startTime;
|
|
418
|
+
this.recordHistory(task, {
|
|
419
|
+
timestamp: startTime,
|
|
420
|
+
duration,
|
|
421
|
+
success: false,
|
|
422
|
+
error: err.message,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
this.log(`Task trigger failed: ${task.id} - ${err.message}`);
|
|
426
|
+
this.emit(SchedulerEvents.TASK_FAILED, { taskId: task.id, task, error: err.message, duration });
|
|
427
|
+
|
|
428
|
+
// 恢复到之前的状态
|
|
429
|
+
task.status = previousStatus;
|
|
430
|
+
this.notify();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 启动调度器。
|
|
436
|
+
* 开始处理所有任务(除了手动停止的任务)。
|
|
437
|
+
*/
|
|
438
|
+
start(): void {
|
|
439
|
+
if (this.running) return;
|
|
440
|
+
this.running = true;
|
|
441
|
+
this.log('Scheduler started');
|
|
442
|
+
this.emit(SchedulerEvents.SCHEDULER_STARTED, { running: true });
|
|
443
|
+
this.registry.getAllTasks().forEach((task) => {
|
|
444
|
+
// 启动所有 stopped 状态的任务(新创建的任务默认是 stopped)
|
|
445
|
+
if (task.status === TaskStatus.STOPPED) {
|
|
446
|
+
task.status = TaskStatus.IDLE;
|
|
447
|
+
this.emit(SchedulerEvents.TASK_UPDATED, { taskId: task.id, task });
|
|
448
|
+
}
|
|
449
|
+
// 调度所有非 running 状态的任务
|
|
450
|
+
if (task.status !== TaskStatus.RUNNING) {
|
|
451
|
+
this.scheduleTask(task);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
this.notify();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 停止调度器。
|
|
459
|
+
* 取消所有正在等待的定时器。
|
|
460
|
+
*/
|
|
461
|
+
stop(): void {
|
|
462
|
+
this.running = false;
|
|
463
|
+
this.log('Scheduler stopped');
|
|
464
|
+
this.emit(SchedulerEvents.SCHEDULER_STOPPED, { running: false });
|
|
465
|
+
|
|
466
|
+
// Cancel all timers using appropriate strategy for each task
|
|
467
|
+
this.timers.forEach((handle, taskId) => {
|
|
468
|
+
const task = this.registry.getTask(taskId);
|
|
469
|
+
if (task) {
|
|
470
|
+
this.getTimerStrategy(task).cancel(handle);
|
|
471
|
+
} else {
|
|
472
|
+
this.defaultTimerStrategy.cancel(handle);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
this.timers.clear();
|
|
476
|
+
|
|
477
|
+
// 将所有非 stopped 状态的任务标记为 stopped
|
|
478
|
+
this.registry.getAllTasks().forEach((task) => {
|
|
479
|
+
if (task.status !== TaskStatus.STOPPED) {
|
|
480
|
+
task.status = TaskStatus.STOPPED;
|
|
481
|
+
this.emit(SchedulerEvents.TASK_UPDATED, { taskId: task.id, task });
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
this.notify(); // 通知 DevTools 更新状态
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private scheduleTask(task: Task): void {
|
|
489
|
+
if (!this.running && task.status !== TaskStatus.RUNNING) return;
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
// 使用新的 getNextScheduleRun
|
|
493
|
+
const nextRun = getNextScheduleRun(task.schedule, {
|
|
494
|
+
timezone: task.options?.timezone || this.config.timezone,
|
|
495
|
+
lastRun: task.lastRun, // 对于 interval 任务,需要上次运行时间
|
|
496
|
+
});
|
|
497
|
+
task.nextRun = nextRun.getTime();
|
|
498
|
+
|
|
499
|
+
const now = Date.now();
|
|
500
|
+
const delay = Math.max(0, nextRun.getTime() - now);
|
|
501
|
+
|
|
502
|
+
this.log(`Scheduling task ${task.id} for ${nextRun.toISOString()} (in ${delay}ms)`);
|
|
503
|
+
|
|
504
|
+
const timerStrategy = this.getTimerStrategy(task);
|
|
505
|
+
const handle = timerStrategy.schedule(() => {
|
|
506
|
+
this.executeTask(task);
|
|
507
|
+
}, delay);
|
|
508
|
+
|
|
509
|
+
this.timers.set(task.id, handle);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
this.log(`Error scheduling task ${task.id}: ${err}`);
|
|
512
|
+
task.status = TaskStatus.ERROR;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private async executeTask(task: Task, attempt: number = 0, force: boolean = false): Promise<void> {
|
|
517
|
+
this.timers.delete(task.id);
|
|
518
|
+
|
|
519
|
+
if (!force && (!this.running || task.status === TaskStatus.STOPPED)) return;
|
|
520
|
+
|
|
521
|
+
task.status = TaskStatus.RUNNING;
|
|
522
|
+
task.lastRun = Date.now();
|
|
523
|
+
task.executionCount = (task.executionCount || 0) + 1;
|
|
524
|
+
const startTime = Date.now();
|
|
525
|
+
|
|
526
|
+
this.log(`Executing task: ${task.id} (Attempt ${attempt})`);
|
|
527
|
+
this.emit(SchedulerEvents.TASK_STARTED, { taskId: task.id, task });
|
|
528
|
+
this.notify();
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
await task.handler();
|
|
532
|
+
|
|
533
|
+
const duration = Date.now() - startTime;
|
|
534
|
+
this.recordHistory(task, {
|
|
535
|
+
timestamp: startTime,
|
|
536
|
+
duration,
|
|
537
|
+
success: true,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
task.status = TaskStatus.IDLE;
|
|
541
|
+
this.log(`Task execution success: ${task.id}`);
|
|
542
|
+
this.emit(SchedulerEvents.TASK_COMPLETED, { taskId: task.id, task, duration });
|
|
543
|
+
this.notify();
|
|
544
|
+
|
|
545
|
+
// Schedule next run
|
|
546
|
+
this.scheduleTask(task);
|
|
547
|
+
|
|
548
|
+
} catch (err: any) {
|
|
549
|
+
const duration = Date.now() - startTime;
|
|
550
|
+
this.recordHistory(task, {
|
|
551
|
+
timestamp: startTime,
|
|
552
|
+
duration,
|
|
553
|
+
success: false,
|
|
554
|
+
error: err.message,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
this.log(`Task execution failed: ${task.id} - ${err.message}`);
|
|
558
|
+
this.emit(SchedulerEvents.TASK_FAILED, { taskId: task.id, task, error: err.message, duration });
|
|
559
|
+
|
|
560
|
+
// 调用任务的 onError 回调
|
|
561
|
+
if (task.options?.onError) {
|
|
562
|
+
try {
|
|
563
|
+
task.options.onError(err, task.id);
|
|
564
|
+
} catch (e) {
|
|
565
|
+
this.log(`onError callback failed: ${e}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Retry logic
|
|
570
|
+
const retryDelay = RetryStrategy.getDelay(attempt, task.options?.retry);
|
|
571
|
+
if (retryDelay >= 0) {
|
|
572
|
+
this.log(`Retrying task ${task.id} in ${retryDelay}ms (Attempt ${attempt + 1})`);
|
|
573
|
+
const timerStrategy = this.getTimerStrategy(task);
|
|
574
|
+
const handle = timerStrategy.schedule(() => {
|
|
575
|
+
this.executeTask(task, attempt + 1);
|
|
576
|
+
}, retryDelay);
|
|
577
|
+
this.timers.set(task.id, handle);
|
|
578
|
+
task.status = TaskStatus.ERROR;
|
|
579
|
+
this.notify();
|
|
580
|
+
} else {
|
|
581
|
+
task.status = TaskStatus.ERROR;
|
|
582
|
+
this.notify();
|
|
583
|
+
// Schedule next regular run even if failed
|
|
584
|
+
this.scheduleTask(task);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private recordHistory(task: Task, record: ExecutionRecord): void {
|
|
590
|
+
task.history.unshift(record);
|
|
591
|
+
if (this.config.maxHistory && task.history.length > this.config.maxHistory) {
|
|
592
|
+
task.history.pop();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private log(message: string): void {
|
|
597
|
+
if (this.config.debug) {
|
|
598
|
+
console.log(`[HyperScheduler] ${message}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Task } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 任务注册表。
|
|
5
|
+
* 负责存储和管理所有的任务实例。
|
|
6
|
+
*/
|
|
7
|
+
export class TaskRegistry {
|
|
8
|
+
private tasks: Map<string, Task>;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.tasks = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 注册一个新任务。
|
|
16
|
+
* @param task 任务对象
|
|
17
|
+
* @throws Error 如果 ID 已存在
|
|
18
|
+
*/
|
|
19
|
+
addTask(task: Task): void {
|
|
20
|
+
if (this.tasks.has(task.id)) {
|
|
21
|
+
throw new Error(`Task with ID "${task.id}" already exists.`);
|
|
22
|
+
}
|
|
23
|
+
this.tasks.set(task.id, task);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 根据 ID 获取任务。
|
|
28
|
+
* @param id 任务 ID
|
|
29
|
+
* @returns 任务对象或 undefined
|
|
30
|
+
*/
|
|
31
|
+
getTask(id: string): Task | undefined {
|
|
32
|
+
return this.tasks.get(id);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 删除任务。
|
|
37
|
+
* @param id 任务 ID
|
|
38
|
+
* @returns 是否删除成功
|
|
39
|
+
*/
|
|
40
|
+
deleteTask(id: string): boolean {
|
|
41
|
+
return this.tasks.delete(id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 获取所有已注册的任务。
|
|
46
|
+
* @returns 任务数组
|
|
47
|
+
*/
|
|
48
|
+
getAllTasks(): Task[] {
|
|
49
|
+
return Array.from(this.tasks.values());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 清空所有任务。
|
|
54
|
+
*/
|
|
55
|
+
clear(): void {
|
|
56
|
+
this.tasks.clear();
|
|
57
|
+
}
|
|
58
|
+
}
|