hyper-scheduler 1.0.0 → 1.1.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.
Files changed (86) hide show
  1. package/README.md +17 -0
  2. package/dist/devtools-Bxtz0rO_.cjs +1 -0
  3. package/dist/devtools-ByJU-Gv1.js +2505 -0
  4. package/dist/index.cjs +1 -0
  5. package/dist/index.js +1048 -0
  6. package/dist/index.umd.cjs +1 -0
  7. package/docs/.vitepress/cache/deps/_metadata.json +31 -0
  8. package/docs/.vitepress/cache/deps/chunk-EKBJ2FPM.js +12798 -0
  9. package/docs/.vitepress/cache/deps/chunk-EKBJ2FPM.js.map +7 -0
  10. package/docs/.vitepress/cache/deps/package.json +3 -0
  11. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4505 -0
  12. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
  13. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +9731 -0
  14. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
  15. package/docs/.vitepress/cache/deps/vue.js +347 -0
  16. package/docs/.vitepress/cache/deps/vue.js.map +7 -0
  17. package/docs/.vitepress/config.ts +4 -0
  18. package/docs/.vitepress/theme/components/DemoFrame.vue +111 -0
  19. package/docs/.vitepress/theme/custom.css +6 -0
  20. package/docs/.vitepress/theme/index.ts +10 -0
  21. package/docs/api/devtools.md +13 -0
  22. package/docs/api/scheduler.md +28 -8
  23. package/docs/examples/index.md +55 -8
  24. package/docs/guide/getting-started.md +38 -0
  25. package/package.json +13 -4
  26. package/.editorconfig +0 -21
  27. package/.eslintrc.cjs +0 -26
  28. package/GEMINI.md +0 -1
  29. package/examples/browser/index.html +0 -354
  30. package/examples/node/simple.js +0 -36
  31. package/examples/react-demo/index.html +0 -12
  32. package/examples/react-demo/package.json +0 -23
  33. package/examples/react-demo/src/App.css +0 -212
  34. package/examples/react-demo/src/App.jsx +0 -160
  35. package/examples/react-demo/src/main.jsx +0 -9
  36. package/examples/react-demo/vite.config.ts +0 -12
  37. package/examples/react-demo/yarn.lock +0 -752
  38. package/examples/vue-demo/index.html +0 -12
  39. package/examples/vue-demo/package.json +0 -21
  40. package/examples/vue-demo/src/App.vue +0 -373
  41. package/examples/vue-demo/src/main.ts +0 -4
  42. package/examples/vue-demo/vite.config.ts +0 -13
  43. package/examples/vue-demo/yarn.lock +0 -375
  44. package/src/constants.ts +0 -18
  45. package/src/core/retry-strategy.ts +0 -28
  46. package/src/core/scheduler.ts +0 -601
  47. package/src/core/task-registry.ts +0 -58
  48. package/src/index.ts +0 -74
  49. package/src/platform/browser/browser-timer.ts +0 -66
  50. package/src/platform/browser/main-thread-timer.ts +0 -16
  51. package/src/platform/browser/worker.ts +0 -31
  52. package/src/platform/node/debug-cli.ts +0 -19
  53. package/src/platform/node/node-timer.ts +0 -15
  54. package/src/platform/timer-strategy.ts +0 -19
  55. package/src/plugins/dev-tools.ts +0 -101
  56. package/src/types.ts +0 -115
  57. package/src/ui/components/devtools.ts +0 -525
  58. package/src/ui/components/floating-trigger.ts +0 -102
  59. package/src/ui/components/icons.ts +0 -16
  60. package/src/ui/components/resizer.ts +0 -129
  61. package/src/ui/components/task-detail.ts +0 -228
  62. package/src/ui/components/task-header.ts +0 -319
  63. package/src/ui/components/task-list.ts +0 -416
  64. package/src/ui/components/timeline.ts +0 -364
  65. package/src/ui/debug-panel.ts +0 -56
  66. package/src/ui/i18n/en.ts +0 -76
  67. package/src/ui/i18n/index.ts +0 -42
  68. package/src/ui/i18n/zh.ts +0 -76
  69. package/src/ui/store/dev-tools-store.ts +0 -191
  70. package/src/ui/styles/theme.css.ts +0 -56
  71. package/src/ui/styles.ts +0 -43
  72. package/src/utils/cron-lite.ts +0 -221
  73. package/src/utils/cron.ts +0 -20
  74. package/src/utils/id.ts +0 -10
  75. package/src/utils/schedule.ts +0 -93
  76. package/src/vite-env.d.ts +0 -1
  77. package/stats.html +0 -4949
  78. package/tests/integration/Debug.test.ts +0 -58
  79. package/tests/unit/Plugin.test.ts +0 -16
  80. package/tests/unit/RetryStrategy.test.ts +0 -21
  81. package/tests/unit/Scheduler.test.ts +0 -38
  82. package/tests/unit/schedule.test.ts +0 -70
  83. package/tests/unit/ui/DevToolsStore.test.ts +0 -67
  84. package/tsconfig.json +0 -28
  85. package/vite.config.ts +0 -51
  86. package/vitest.config.ts +0 -24
package/dist/index.js ADDED
@@ -0,0 +1,1048 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ const TaskStatus = {
5
+ IDLE: "idle",
6
+ RUNNING: "running",
7
+ STOPPED: "stopped",
8
+ ERROR: "error"
9
+ };
10
+ const SchedulerEvents = {
11
+ TASK_REGISTERED: "task_registered",
12
+ TASK_STARTED: "task_started",
13
+ TASK_COMPLETED: "task_completed",
14
+ TASK_FAILED: "task_failed",
15
+ TASK_STOPPED: "task_stopped",
16
+ TASK_REMOVED: "task_removed",
17
+ TASK_UPDATED: "task_updated",
18
+ SCHEDULER_STARTED: "scheduler_started",
19
+ SCHEDULER_STOPPED: "scheduler_stopped"
20
+ };
21
+ class TaskRegistry {
22
+ // 命名空间 -> Set<任务ID>
23
+ constructor() {
24
+ __publicField(this, "tasks");
25
+ __publicField(this, "namespaceIndex");
26
+ this.tasks = /* @__PURE__ */ new Map();
27
+ this.namespaceIndex = /* @__PURE__ */ new Map();
28
+ }
29
+ /**
30
+ * 注册一个新任务。
31
+ * @param task 任务对象
32
+ * @throws Error 如果 ID 已存在
33
+ */
34
+ addTask(task) {
35
+ var _a, _b;
36
+ if (this.tasks.has(task.id)) {
37
+ throw new Error(`Task with ID "${task.id}" already exists.`);
38
+ }
39
+ this.tasks.set(task.id, task);
40
+ const namespace = ((_a = task.options) == null ? void 0 : _a.namespace) || "default";
41
+ if (!this.namespaceIndex.has(namespace)) {
42
+ this.namespaceIndex.set(namespace, /* @__PURE__ */ new Set());
43
+ }
44
+ (_b = this.namespaceIndex.get(namespace)) == null ? void 0 : _b.add(task.id);
45
+ }
46
+ /**
47
+ * 根据 ID 获取任务。
48
+ * @param id 任务 ID
49
+ * @returns 任务对象或 undefined
50
+ */
51
+ getTask(id) {
52
+ return this.tasks.get(id);
53
+ }
54
+ /**
55
+ * 删除任务。
56
+ * @param id 任务 ID
57
+ * @returns 是否删除成功
58
+ */
59
+ deleteTask(id) {
60
+ var _a, _b, _c;
61
+ const task = this.tasks.get(id);
62
+ if (!task) {
63
+ return false;
64
+ }
65
+ const namespace = ((_a = task.options) == null ? void 0 : _a.namespace) || "default";
66
+ (_b = this.namespaceIndex.get(namespace)) == null ? void 0 : _b.delete(id);
67
+ if (((_c = this.namespaceIndex.get(namespace)) == null ? void 0 : _c.size) === 0) {
68
+ this.namespaceIndex.delete(namespace);
69
+ }
70
+ return this.tasks.delete(id);
71
+ }
72
+ /**
73
+ * 获取所有已注册的任务。
74
+ * @returns 任务数组
75
+ */
76
+ getAllTasks() {
77
+ return Array.from(this.tasks.values());
78
+ }
79
+ /**
80
+ * 根据命名空间获取所有任务。
81
+ * @param namespace 命名空间名称
82
+ * @returns 该命名空间下的任务数组
83
+ */
84
+ getTasksByNamespace(namespace) {
85
+ const taskIds = this.namespaceIndex.get(namespace);
86
+ if (!taskIds) {
87
+ return [];
88
+ }
89
+ return Array.from(taskIds).map((id) => this.tasks.get(id)).filter(Boolean);
90
+ }
91
+ /**
92
+ * 清空所有任务和索引。
93
+ */
94
+ clear() {
95
+ this.tasks.clear();
96
+ this.namespaceIndex.clear();
97
+ }
98
+ }
99
+ function validateId(id) {
100
+ if (!id || typeof id !== "string" || id.trim() === "") {
101
+ throw new Error("Task ID must be a non-empty string.");
102
+ }
103
+ }
104
+ function parseField(field, min, max) {
105
+ const values = /* @__PURE__ */ new Set();
106
+ if (field === "*") {
107
+ for (let i = min; i <= max; i++) {
108
+ values.add(i);
109
+ }
110
+ return Array.from(values).sort((a, b) => a - b);
111
+ }
112
+ const parts = field.split(",");
113
+ for (const part of parts) {
114
+ if (part.includes("/")) {
115
+ const [range, step] = part.split("/");
116
+ const stepNum = parseInt(step, 10);
117
+ if (range === "*") {
118
+ for (let i = min; i <= max; i += stepNum) {
119
+ values.add(i);
120
+ }
121
+ } else if (range.includes("-")) {
122
+ const [start, end] = range.split("-").map(Number);
123
+ if (start < min || end > max) {
124
+ throw new Error(`Value out of range: ${start}-${end} (expected ${min}-${max})`);
125
+ }
126
+ for (let i = start; i <= end; i += stepNum) {
127
+ values.add(i);
128
+ }
129
+ } else {
130
+ const start = parseInt(range, 10);
131
+ if (start < min || start > max) {
132
+ throw new Error(`Value out of range: ${start} (expected ${min}-${max})`);
133
+ }
134
+ for (let i = start; i <= max; i += stepNum) {
135
+ values.add(i);
136
+ }
137
+ }
138
+ } else if (part.includes("-")) {
139
+ const [start, end] = part.split("-").map(Number);
140
+ if (start < min || end > max) {
141
+ throw new Error(`Value out of range: ${start}-${end} (expected ${min}-${max})`);
142
+ }
143
+ for (let i = start; i <= end; i++) {
144
+ values.add(i);
145
+ }
146
+ } else {
147
+ const val = parseInt(part, 10);
148
+ if (val < min || val > max) {
149
+ throw new Error(`Value out of range: ${val} (expected ${min}-${max})`);
150
+ }
151
+ values.add(val);
152
+ }
153
+ }
154
+ return Array.from(values).sort((a, b) => a - b);
155
+ }
156
+ function parseCronExpression(expression) {
157
+ const parts = expression.trim().split(/\s+/);
158
+ let second, minute, hour, dayOfMonth, month, dayOfWeek;
159
+ if (parts.length === 5) {
160
+ second = [0];
161
+ [minute, hour, dayOfMonth, month, dayOfWeek] = [
162
+ parseField(parts[0], 0, 59),
163
+ parseField(parts[1], 0, 23),
164
+ parseField(parts[2], 1, 31),
165
+ parseField(parts[3], 1, 12),
166
+ parseField(parts[4], 0, 6)
167
+ ];
168
+ } else if (parts.length === 6) {
169
+ [second, minute, hour, dayOfMonth, month, dayOfWeek] = [
170
+ parseField(parts[0], 0, 59),
171
+ parseField(parts[1], 0, 59),
172
+ parseField(parts[2], 0, 23),
173
+ parseField(parts[3], 1, 31),
174
+ parseField(parts[4], 1, 12),
175
+ parseField(parts[5], 0, 6)
176
+ ];
177
+ } else {
178
+ throw new Error(`Invalid cron expression: expected 5 or 6 fields, got ${parts.length}`);
179
+ }
180
+ return { second, minute, hour, dayOfMonth, month, dayOfWeek };
181
+ }
182
+ function validateCron(expression) {
183
+ try {
184
+ parseCronExpression(expression);
185
+ } catch (err) {
186
+ throw new Error(`Invalid cron expression: ${expression}`);
187
+ }
188
+ }
189
+ function matchesCron(date, fields) {
190
+ const second = date.getSeconds();
191
+ const minute = date.getMinutes();
192
+ const hour = date.getHours();
193
+ const dayOfMonth = date.getDate();
194
+ const month = date.getMonth() + 1;
195
+ const dayOfWeek = date.getDay();
196
+ return fields.second.includes(second) && fields.minute.includes(minute) && fields.hour.includes(hour) && fields.month.includes(month) && (fields.dayOfMonth.includes(dayOfMonth) || fields.dayOfWeek.includes(dayOfWeek));
197
+ }
198
+ function getNextRun$1(expression, _timezone) {
199
+ var _a, _b, _c;
200
+ const fields = parseCronExpression(expression);
201
+ const now = /* @__PURE__ */ new Date();
202
+ let candidate = new Date(now.getTime() + 1e3);
203
+ candidate.setMilliseconds(0);
204
+ const maxIterations = 4 * 365 * 24 * 60 * 60;
205
+ let iterations = 0;
206
+ while (iterations < maxIterations) {
207
+ if (matchesCron(candidate, fields)) {
208
+ return candidate;
209
+ }
210
+ const second = candidate.getSeconds();
211
+ const minute = candidate.getMinutes();
212
+ const hour = candidate.getHours();
213
+ if (!fields.second.includes(second)) {
214
+ const nextSecond = (_a = fields.second.find((s) => s > second)) != null ? _a : fields.second[0];
215
+ if (nextSecond > second) {
216
+ candidate.setSeconds(nextSecond);
217
+ } else {
218
+ candidate.setMinutes(minute + 1, nextSecond);
219
+ }
220
+ candidate.setMilliseconds(0);
221
+ iterations++;
222
+ continue;
223
+ }
224
+ if (!fields.minute.includes(minute)) {
225
+ const nextMinute = (_b = fields.minute.find((m) => m > minute)) != null ? _b : fields.minute[0];
226
+ if (nextMinute > minute) {
227
+ candidate.setMinutes(nextMinute, fields.second[0]);
228
+ } else {
229
+ candidate.setHours(hour + 1, nextMinute, fields.second[0]);
230
+ }
231
+ candidate.setMilliseconds(0);
232
+ iterations++;
233
+ continue;
234
+ }
235
+ if (!fields.hour.includes(hour)) {
236
+ const nextHour = (_c = fields.hour.find((h) => h > hour)) != null ? _c : fields.hour[0];
237
+ if (nextHour > hour) {
238
+ candidate.setHours(nextHour, fields.minute[0], fields.second[0]);
239
+ } else {
240
+ candidate.setDate(candidate.getDate() + 1);
241
+ candidate.setHours(nextHour, fields.minute[0], fields.second[0]);
242
+ }
243
+ candidate.setMilliseconds(0);
244
+ iterations++;
245
+ continue;
246
+ }
247
+ candidate = new Date(candidate.getTime() + 1e3);
248
+ iterations++;
249
+ }
250
+ throw new Error(`Could not find next run time for cron expression: ${expression}`);
251
+ }
252
+ function parseSchedule(schedule) {
253
+ const intervalRegex = /^(\d+)(s|m|h|d)$/;
254
+ const match = schedule.match(intervalRegex);
255
+ if (match) {
256
+ const value = parseInt(match[1], 10);
257
+ const unit = match[2];
258
+ let multiplier = 1e3;
259
+ switch (unit) {
260
+ case "s":
261
+ multiplier = 1e3;
262
+ break;
263
+ case "m":
264
+ multiplier = 60 * 1e3;
265
+ break;
266
+ case "h":
267
+ multiplier = 60 * 60 * 1e3;
268
+ break;
269
+ case "d":
270
+ multiplier = 24 * 60 * 60 * 1e3;
271
+ break;
272
+ }
273
+ return {
274
+ type: "interval",
275
+ value: value * multiplier
276
+ };
277
+ }
278
+ try {
279
+ validateCron(schedule);
280
+ return {
281
+ type: "cron",
282
+ value: schedule
283
+ };
284
+ } catch (err) {
285
+ throw new Error(`无效的调度格式: "${schedule}". 必须是有效的 Cron 表达式或间隔字符串 (例如 "10s")。`);
286
+ }
287
+ }
288
+ function getNextRun(schedule, options) {
289
+ let parsed;
290
+ if (typeof schedule === "string") {
291
+ parsed = parseSchedule(schedule);
292
+ } else {
293
+ parsed = schedule;
294
+ }
295
+ if (parsed.type === "interval") {
296
+ const intervalMs = parsed.value;
297
+ const lastRun = (options == null ? void 0 : options.lastRun) || Date.now();
298
+ return new Date(lastRun + intervalMs);
299
+ } else {
300
+ const cronExpression = parsed.value;
301
+ try {
302
+ return getNextRun$1(cronExpression, options == null ? void 0 : options.timezone);
303
+ } catch (err) {
304
+ throw new Error(`无法计算下一次 Cron 运行时间: ${cronExpression}`);
305
+ }
306
+ }
307
+ }
308
+ class RetryStrategy {
309
+ /**
310
+ * 计算下一次重试的延迟时间。
311
+ * 采用指数退避算法 (Exponential Backoff)。
312
+ * @param attempt 当前重试次数 (从 0 开始,0 表示第一次重试)
313
+ * @param options 任务的重试配置
314
+ * @returns 延迟毫秒数,如果超过最大尝试次数则返回 -1
315
+ */
316
+ static getDelay(attempt, options) {
317
+ if (!options) {
318
+ return -1;
319
+ }
320
+ const { maxAttempts, initialDelay, factor = 2 } = options;
321
+ if (attempt >= maxAttempts) {
322
+ return -1;
323
+ }
324
+ return initialDelay * Math.pow(factor, attempt);
325
+ }
326
+ }
327
+ let Scheduler$1 = class Scheduler {
328
+ /**
329
+ * 创建一个新的调度器实例。
330
+ * @param timerStrategy 默认计时策略(NodeTimer 或 BrowserTimer)
331
+ * @param config 调度器配置
332
+ * @param timerStrategyFactory 可选的定时器策略工厂,用于创建任务级别的定时器
333
+ */
334
+ constructor(timerStrategy, config = {}, timerStrategyFactory) {
335
+ __publicField(this, "registry");
336
+ __publicField(this, "config");
337
+ __publicField(this, "defaultTimerStrategy");
338
+ __publicField(this, "timerStrategyFactory");
339
+ __publicField(this, "taskTimerStrategies");
340
+ // Task ID -> TimerStrategy
341
+ __publicField(this, "running");
342
+ __publicField(this, "timers");
343
+ // Task ID -> Timer Handle
344
+ __publicField(this, "listeners");
345
+ __publicField(this, "eventListeners");
346
+ this.registry = new TaskRegistry();
347
+ this.defaultTimerStrategy = timerStrategy;
348
+ this.timerStrategyFactory = timerStrategyFactory;
349
+ this.taskTimerStrategies = /* @__PURE__ */ new Map();
350
+ this.config = {
351
+ debug: false,
352
+ maxHistory: 50,
353
+ ...config
354
+ };
355
+ this.running = false;
356
+ this.timers = /* @__PURE__ */ new Map();
357
+ this.listeners = [];
358
+ this.eventListeners = /* @__PURE__ */ new Map();
359
+ if (this.config.plugins && Array.isArray(this.config.plugins)) {
360
+ this.config.plugins.forEach((plugin) => {
361
+ try {
362
+ this.log(`Initializing plugin: ${plugin.name}`);
363
+ plugin.init(this);
364
+ } catch (err) {
365
+ console.warn(`[HyperScheduler] Failed to initialize plugin ${plugin.name}:`, err);
366
+ }
367
+ });
368
+ }
369
+ }
370
+ /**
371
+ * 订阅任务列表变更事件。
372
+ * 当任务创建、状态改变或删除时触发。
373
+ * @param listener 回调函数
374
+ * @returns 取消订阅的函数
375
+ */
376
+ subscribe(listener) {
377
+ this.listeners.push(listener);
378
+ return () => {
379
+ this.listeners = this.listeners.filter((l) => l !== listener);
380
+ };
381
+ }
382
+ /**
383
+ * 订阅特定事件
384
+ * @param event 事件名称
385
+ * @param handler 事件处理函数
386
+ * @returns 取消订阅的函数
387
+ */
388
+ on(event, handler) {
389
+ if (!this.eventListeners.has(event)) {
390
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
391
+ }
392
+ this.eventListeners.get(event).add(handler);
393
+ return () => {
394
+ var _a;
395
+ (_a = this.eventListeners.get(event)) == null ? void 0 : _a.delete(handler);
396
+ };
397
+ }
398
+ emit(event, payload) {
399
+ const handlers = this.eventListeners.get(event);
400
+ if (handlers) {
401
+ handlers.forEach((handler) => handler(payload));
402
+ }
403
+ }
404
+ /**
405
+ * 获取任务的定时器策略
406
+ * 优先使用任务级别配置,其次使用全局配置,最后使用默认策略
407
+ */
408
+ getTimerStrategy(task) {
409
+ var _a;
410
+ const taskDriver = (_a = task.options) == null ? void 0 : _a.driver;
411
+ const globalDriver = this.config.driver;
412
+ const driver = taskDriver || globalDriver;
413
+ if (!driver || !this.timerStrategyFactory) {
414
+ return this.defaultTimerStrategy;
415
+ }
416
+ const cacheKey = `${task.id}_${driver}`;
417
+ if (this.taskTimerStrategies.has(cacheKey)) {
418
+ return this.taskTimerStrategies.get(cacheKey);
419
+ }
420
+ const strategy = this.timerStrategyFactory(driver);
421
+ this.taskTimerStrategies.set(cacheKey, strategy);
422
+ return strategy;
423
+ }
424
+ notify() {
425
+ if (this.listeners.length > 0) {
426
+ const tasks = this.registry.getAllTasks();
427
+ this.listeners.forEach((l) => l(tasks));
428
+ }
429
+ }
430
+ /**
431
+ * 创建并注册一个新任务。
432
+ * @param definition 任务定义
433
+ */
434
+ createTask(definition) {
435
+ var _a;
436
+ validateId(definition.id);
437
+ parseSchedule(definition.schedule);
438
+ const task = {
439
+ ...definition,
440
+ tags: definition.tags || [],
441
+ // 新创建的任务默认是 stopped 状态,需要手动启动或等调度器启动
442
+ status: TaskStatus.STOPPED,
443
+ history: [],
444
+ executionCount: 0
445
+ };
446
+ this.registry.addTask(task);
447
+ this.log(`Task created: ${task.id}`);
448
+ this.emit(SchedulerEvents.TASK_REGISTERED, { taskId: task.id, task });
449
+ this.notify();
450
+ if (this.running) {
451
+ task.status = TaskStatus.IDLE;
452
+ if ((_a = task.options) == null ? void 0 : _a.runImmediately) {
453
+ this.triggerTask(task.id);
454
+ }
455
+ this.scheduleTask(task);
456
+ }
457
+ }
458
+ /**
459
+ * 删除指定 ID 的任务。
460
+ * @param id 任务 ID
461
+ * @returns 如果删除成功返回 true,否则返回 false
462
+ */
463
+ deleteTask(id) {
464
+ this.stopTask(id);
465
+ const deleted = this.registry.deleteTask(id);
466
+ if (deleted) {
467
+ this.log(`Task deleted: ${id}`);
468
+ this.emit(SchedulerEvents.TASK_REMOVED, { taskId: id });
469
+ this.notify();
470
+ }
471
+ return deleted;
472
+ }
473
+ /**
474
+ * 手动启动一个任务(将其置为 idle 状态并重新加入调度)。
475
+ * 即使调度器未启动,也可以单独启动某个任务。
476
+ * @param id 任务 ID
477
+ */
478
+ startTask(id) {
479
+ const task = this.registry.getTask(id);
480
+ if (!task) {
481
+ throw new Error(`Task not found: ${id}`);
482
+ }
483
+ if (task.status === TaskStatus.RUNNING) {
484
+ return;
485
+ }
486
+ const existingHandle = this.timers.get(id);
487
+ if (existingHandle) {
488
+ this.getTimerStrategy(task).cancel(existingHandle);
489
+ this.timers.delete(id);
490
+ }
491
+ task.status = TaskStatus.IDLE;
492
+ this.log(`Starting task: ${id}`);
493
+ this.emit(SchedulerEvents.TASK_STARTED, { taskId: id, task });
494
+ this.notify();
495
+ this.scheduleTaskForce(task);
496
+ }
497
+ /**
498
+ * 强制调度任务,即使调度器未启动
499
+ */
500
+ scheduleTaskForce(task) {
501
+ var _a;
502
+ try {
503
+ const nextRun = getNextRun(task.schedule, {
504
+ timezone: ((_a = task.options) == null ? void 0 : _a.timezone) || this.config.timezone,
505
+ lastRun: task.lastRun
506
+ });
507
+ task.nextRun = nextRun.getTime();
508
+ const now = Date.now();
509
+ const delay = Math.max(0, nextRun.getTime() - now);
510
+ this.log(`Scheduling task ${task.id} for ${nextRun.toISOString()} (in ${delay}ms)`);
511
+ const timerStrategy = this.getTimerStrategy(task);
512
+ const handle = timerStrategy.schedule(() => {
513
+ this.executeTaskIndividual(task);
514
+ }, delay);
515
+ this.timers.set(task.id, handle);
516
+ } catch (err) {
517
+ this.log(`Error scheduling task ${task.id}: ${err}`);
518
+ task.status = TaskStatus.ERROR;
519
+ this.notify();
520
+ }
521
+ }
522
+ /**
523
+ * 执行单个任务(用于独立启动的任务)
524
+ */
525
+ async executeTaskIndividual(task, attempt = 0) {
526
+ var _a, _b;
527
+ this.timers.delete(task.id);
528
+ if (task.status === TaskStatus.STOPPED) return;
529
+ task.status = TaskStatus.RUNNING;
530
+ task.lastRun = Date.now();
531
+ task.executionCount = (task.executionCount || 0) + 1;
532
+ const startTime = Date.now();
533
+ this.log(`Executing task: ${task.id} (Attempt ${attempt})`);
534
+ this.emit(SchedulerEvents.TASK_STARTED, { taskId: task.id, task });
535
+ this.notify();
536
+ try {
537
+ await task.handler();
538
+ const duration = Date.now() - startTime;
539
+ this.recordHistory(task, {
540
+ timestamp: startTime,
541
+ duration,
542
+ success: true
543
+ });
544
+ task.status = TaskStatus.IDLE;
545
+ this.log(`Task execution success: ${task.id}`);
546
+ this.emit(SchedulerEvents.TASK_COMPLETED, { taskId: task.id, task, duration });
547
+ this.notify();
548
+ this.scheduleTaskForce(task);
549
+ } catch (err) {
550
+ const duration = Date.now() - startTime;
551
+ this.recordHistory(task, {
552
+ timestamp: startTime,
553
+ duration,
554
+ success: false,
555
+ error: err.message
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
+ if ((_a = task.options) == null ? void 0 : _a.onError) {
560
+ try {
561
+ task.options.onError(err, task.id);
562
+ } catch (e) {
563
+ this.log(`onError callback failed: ${e}`);
564
+ }
565
+ }
566
+ const retryDelay = RetryStrategy.getDelay(attempt, (_b = task.options) == null ? void 0 : _b.retry);
567
+ if (retryDelay >= 0) {
568
+ this.log(`Retrying task ${task.id} in ${retryDelay}ms (Attempt ${attempt + 1})`);
569
+ const timerStrategy = this.getTimerStrategy(task);
570
+ const handle = timerStrategy.schedule(() => {
571
+ this.executeTaskIndividual(task, attempt + 1);
572
+ }, retryDelay);
573
+ this.timers.set(task.id, handle);
574
+ task.status = TaskStatus.ERROR;
575
+ this.notify();
576
+ } else {
577
+ task.status = TaskStatus.ERROR;
578
+ this.notify();
579
+ this.scheduleTaskForce(task);
580
+ }
581
+ }
582
+ }
583
+ /**
584
+ * 停止一个任务(取消当前的定时器并标记为 stopped)。
585
+ * @param id 任务 ID
586
+ */
587
+ stopTask(id) {
588
+ const task = this.registry.getTask(id);
589
+ const handle = this.timers.get(id);
590
+ if (handle && task) {
591
+ this.getTimerStrategy(task).cancel(handle);
592
+ this.timers.delete(id);
593
+ } else if (handle) {
594
+ this.defaultTimerStrategy.cancel(handle);
595
+ this.timers.delete(id);
596
+ }
597
+ if (task) {
598
+ task.status = TaskStatus.STOPPED;
599
+ this.log(`Task stopped: ${id}`);
600
+ this.emit(SchedulerEvents.TASK_STOPPED, { taskId: id, task });
601
+ this.notify();
602
+ }
603
+ }
604
+ /**
605
+ * 获取任务信息。
606
+ * @param id 任务 ID
607
+ */
608
+ getTask(id) {
609
+ return this.registry.getTask(id);
610
+ }
611
+ /**
612
+ * 获取所有任务,可按命名空间筛选。
613
+ * @param namespace 可选的命名空间名称
614
+ * @returns 任务数组
615
+ */
616
+ getAllTasks(namespace) {
617
+ if (namespace) {
618
+ return this.registry.getTasksByNamespace(namespace);
619
+ }
620
+ return this.registry.getAllTasks();
621
+ }
622
+ /**
623
+ * 获取调度器运行状态。
624
+ */
625
+ isRunning() {
626
+ return this.running;
627
+ }
628
+ /**
629
+ * 获取任务的实际驱动方式。
630
+ * 优先使用任务级配置,其次使用全局配置,默认为 'worker'。
631
+ */
632
+ getTaskDriver(id) {
633
+ var _a;
634
+ const task = this.registry.getTask(id);
635
+ if (!task) return this.config.driver || "worker";
636
+ return ((_a = task.options) == null ? void 0 : _a.driver) || this.config.driver || "worker";
637
+ }
638
+ /**
639
+ * 获取全局驱动配置。
640
+ */
641
+ getGlobalDriver() {
642
+ return this.config.driver || "worker";
643
+ }
644
+ /**
645
+ * 手动触发任务执行(忽略调度器状态,立即执行一次)。
646
+ * 执行完成后恢复到之前的状态。
647
+ * @param id 任务 ID
648
+ */
649
+ async triggerTask(id) {
650
+ const task = this.getTask(id);
651
+ if (!task) return;
652
+ if (task.status === TaskStatus.RUNNING) return;
653
+ const previousStatus = task.status;
654
+ task.status = TaskStatus.RUNNING;
655
+ task.lastRun = Date.now();
656
+ task.executionCount = (task.executionCount || 0) + 1;
657
+ const startTime = Date.now();
658
+ this.log(`Triggering task: ${task.id}`);
659
+ this.emit(SchedulerEvents.TASK_STARTED, { taskId: task.id, task });
660
+ this.notify();
661
+ try {
662
+ await task.handler();
663
+ const duration = Date.now() - startTime;
664
+ this.recordHistory(task, {
665
+ timestamp: startTime,
666
+ duration,
667
+ success: true
668
+ });
669
+ task.status = previousStatus;
670
+ this.log(`Task trigger success: ${task.id}`);
671
+ this.emit(SchedulerEvents.TASK_COMPLETED, { taskId: task.id, task, duration });
672
+ this.notify();
673
+ } catch (err) {
674
+ const duration = Date.now() - startTime;
675
+ this.recordHistory(task, {
676
+ timestamp: startTime,
677
+ duration,
678
+ success: false,
679
+ error: err.message
680
+ });
681
+ this.log(`Task trigger failed: ${task.id} - ${err.message}`);
682
+ this.emit(SchedulerEvents.TASK_FAILED, { taskId: task.id, task, error: err.message, duration });
683
+ task.status = previousStatus;
684
+ this.notify();
685
+ }
686
+ }
687
+ /**
688
+ * 启动调度器。
689
+ * 开始处理所有任务(除了手动停止的任务)。
690
+ */
691
+ start(scope) {
692
+ if (!scope && this.running) return;
693
+ this.log(scope ? `Scheduler starting for scope: ${scope}` : "Scheduler started");
694
+ this.emit(SchedulerEvents.SCHEDULER_STARTED, { running: true, scope });
695
+ const tasksToStart = scope ? this.registry.getTasksByNamespace(scope) : this.registry.getAllTasks();
696
+ tasksToStart.forEach((task) => {
697
+ var _a;
698
+ if (task.status === TaskStatus.STOPPED) {
699
+ task.status = TaskStatus.IDLE;
700
+ this.emit(SchedulerEvents.TASK_UPDATED, { taskId: task.id, task });
701
+ }
702
+ if (task.status !== TaskStatus.RUNNING) {
703
+ if ((_a = task.options) == null ? void 0 : _a.runImmediately) {
704
+ this.triggerTask(task.id);
705
+ }
706
+ this.scheduleTask(task);
707
+ }
708
+ });
709
+ if (!scope) {
710
+ this.running = true;
711
+ }
712
+ this.notify();
713
+ }
714
+ /**
715
+ * 停止调度器。
716
+ * 取消所有正在等待的定时器。
717
+ */
718
+ stop(scope) {
719
+ if (!scope && !this.running) return;
720
+ this.log(scope ? `Scheduler stopping for scope: ${scope}` : "Scheduler stopped");
721
+ this.emit(SchedulerEvents.SCHEDULER_STOPPED, { running: false, scope });
722
+ const tasksToStop = scope ? this.registry.getTasksByNamespace(scope) : this.registry.getAllTasks();
723
+ tasksToStop.forEach((task) => {
724
+ const handle = this.timers.get(task.id);
725
+ if (handle) {
726
+ this.getTimerStrategy(task).cancel(handle);
727
+ this.timers.delete(task.id);
728
+ }
729
+ if (task.status !== TaskStatus.STOPPED) {
730
+ task.status = TaskStatus.STOPPED;
731
+ this.emit(SchedulerEvents.TASK_UPDATED, { taskId: task.id, task });
732
+ }
733
+ });
734
+ if (!scope) {
735
+ this.running = false;
736
+ }
737
+ this.notify();
738
+ }
739
+ scheduleTask(task) {
740
+ var _a;
741
+ try {
742
+ const nextRun = getNextRun(task.schedule, {
743
+ timezone: ((_a = task.options) == null ? void 0 : _a.timezone) || this.config.timezone,
744
+ lastRun: task.lastRun
745
+ // 对于 interval 任务,需要上次运行时间
746
+ });
747
+ task.nextRun = nextRun.getTime();
748
+ const now = Date.now();
749
+ const delay = Math.max(0, nextRun.getTime() - now);
750
+ this.log(`Scheduling task ${task.id} for ${nextRun.toISOString()} (in ${delay}ms)`);
751
+ const timerStrategy = this.getTimerStrategy(task);
752
+ const handle = timerStrategy.schedule(() => {
753
+ this.executeTask(task);
754
+ }, delay);
755
+ this.timers.set(task.id, handle);
756
+ } catch (err) {
757
+ this.log(`Error scheduling task ${task.id}: ${err}`);
758
+ task.status = TaskStatus.ERROR;
759
+ }
760
+ }
761
+ async executeTask(task, attempt = 0, force = false) {
762
+ var _a, _b;
763
+ this.timers.delete(task.id);
764
+ if (!force && task.status === TaskStatus.STOPPED) return;
765
+ task.status = TaskStatus.RUNNING;
766
+ task.lastRun = Date.now();
767
+ task.executionCount = (task.executionCount || 0) + 1;
768
+ const startTime = Date.now();
769
+ this.log(`Executing task: ${task.id} (Attempt ${attempt})`);
770
+ this.emit(SchedulerEvents.TASK_STARTED, { taskId: task.id, task });
771
+ this.notify();
772
+ try {
773
+ await task.handler();
774
+ const duration = Date.now() - startTime;
775
+ this.recordHistory(task, {
776
+ timestamp: startTime,
777
+ duration,
778
+ success: true
779
+ });
780
+ task.status = TaskStatus.IDLE;
781
+ this.log(`Task execution success: ${task.id}`);
782
+ this.emit(SchedulerEvents.TASK_COMPLETED, { taskId: task.id, task, duration });
783
+ this.notify();
784
+ this.scheduleTask(task);
785
+ } catch (err) {
786
+ const duration = Date.now() - startTime;
787
+ this.recordHistory(task, {
788
+ timestamp: startTime,
789
+ duration,
790
+ success: false,
791
+ error: err.message
792
+ });
793
+ this.log(`Task execution failed: ${task.id} - ${err.message}`);
794
+ this.emit(SchedulerEvents.TASK_FAILED, { taskId: task.id, task, error: err.message, duration });
795
+ if ((_a = task.options) == null ? void 0 : _a.onError) {
796
+ try {
797
+ task.options.onError(err, task.id);
798
+ } catch (e) {
799
+ this.log(`onError callback failed: ${e}`);
800
+ }
801
+ }
802
+ const retryDelay = RetryStrategy.getDelay(attempt, (_b = task.options) == null ? void 0 : _b.retry);
803
+ if (retryDelay >= 0) {
804
+ this.log(`Retrying task ${task.id} in ${retryDelay}ms (Attempt ${attempt + 1})`);
805
+ const timerStrategy = this.getTimerStrategy(task);
806
+ const handle = timerStrategy.schedule(() => {
807
+ this.executeTask(task, attempt + 1);
808
+ }, retryDelay);
809
+ this.timers.set(task.id, handle);
810
+ task.status = TaskStatus.ERROR;
811
+ this.notify();
812
+ } else {
813
+ task.status = TaskStatus.ERROR;
814
+ this.notify();
815
+ this.scheduleTask(task);
816
+ }
817
+ }
818
+ }
819
+ recordHistory(task, record) {
820
+ task.history.unshift(record);
821
+ if (this.config.maxHistory && task.history.length > this.config.maxHistory) {
822
+ task.history.pop();
823
+ }
824
+ }
825
+ log(message) {
826
+ if (this.config.debug) {
827
+ console.log(`[HyperScheduler] ${message}`);
828
+ }
829
+ }
830
+ };
831
+ class NodeTimer {
832
+ schedule(callback, delay) {
833
+ return setTimeout(callback, delay);
834
+ }
835
+ cancel(handle) {
836
+ clearTimeout(handle);
837
+ }
838
+ }
839
+ const encodedJs = "IWZ1bmN0aW9uKCl7InVzZSBzdHJpY3QiO2NvbnN0IGU9bmV3IE1hcDtzZWxmLm9ubWVzc2FnZT1zPT57Y29uc3R7aWQ6dCxkZWxheTpjLHR5cGU6bH09cy5kYXRhO2lmKCJzY2hlZHVsZSI9PT1sKXtjb25zdCBzPXNlbGYuc2V0VGltZW91dCgoKT0+e3NlbGYucG9zdE1lc3NhZ2Uoe2lkOnQsdHlwZToidGljayJ9KSxlLmRlbGV0ZSh0KX0sYyk7ZS5zZXQodCxzKX1lbHNlIGlmKCJjYW5jZWwiPT09bCl7Y29uc3Qgcz1lLmdldCh0KTt2b2lkIDAhPT1zJiYoc2VsZi5jbGVhclRpbWVvdXQocyksZS5kZWxldGUodCkpfX19KCk7Cg==";
840
+ const decodeBase64 = (base64) => Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
841
+ const blob = typeof self !== "undefined" && self.Blob && new Blob([decodeBase64(encodedJs)], { type: "text/javascript;charset=utf-8" });
842
+ function WorkerWrapper(options) {
843
+ let objURL;
844
+ try {
845
+ objURL = blob && (self.URL || self.webkitURL).createObjectURL(blob);
846
+ if (!objURL) throw "";
847
+ const worker = new Worker(objURL, {
848
+ name: options == null ? void 0 : options.name
849
+ });
850
+ worker.addEventListener("error", () => {
851
+ (self.URL || self.webkitURL).revokeObjectURL(objURL);
852
+ });
853
+ return worker;
854
+ } catch (e) {
855
+ return new Worker(
856
+ "data:text/javascript;base64," + encodedJs,
857
+ {
858
+ name: options == null ? void 0 : options.name
859
+ }
860
+ );
861
+ } finally {
862
+ objURL && (self.URL || self.webkitURL).revokeObjectURL(objURL);
863
+ }
864
+ }
865
+ class BrowserTimer {
866
+ constructor() {
867
+ __publicField(this, "worker");
868
+ __publicField(this, "callbacks");
869
+ __publicField(this, "nextId");
870
+ if (typeof WorkerWrapper !== "undefined") {
871
+ this.worker = new WorkerWrapper();
872
+ } else {
873
+ this.worker = {
874
+ postMessage: () => {
875
+ },
876
+ onmessage: null,
877
+ terminate: () => {
878
+ },
879
+ addEventListener: () => {
880
+ },
881
+ removeEventListener: () => {
882
+ },
883
+ dispatchEvent: () => false
884
+ };
885
+ }
886
+ this.callbacks = /* @__PURE__ */ new Map();
887
+ this.nextId = 1;
888
+ this.worker.onmessage = (e) => {
889
+ const { id, type } = e.data;
890
+ if (type === "tick") {
891
+ const callback = this.callbacks.get(id);
892
+ if (callback) {
893
+ callback();
894
+ this.callbacks.delete(id);
895
+ }
896
+ }
897
+ };
898
+ }
899
+ /**
900
+ * 安排一个定时任务。
901
+ * 向 Web Worker 发送消息以注册计时器。
902
+ */
903
+ schedule(callback, delay) {
904
+ const id = this.nextId++;
905
+ this.callbacks.set(id, callback);
906
+ this.worker.postMessage({ id, delay, type: "schedule" });
907
+ return id;
908
+ }
909
+ /**
910
+ * 取消一个定时任务。
911
+ */
912
+ cancel(handle) {
913
+ this.worker.postMessage({ id: handle, type: "cancel" });
914
+ this.callbacks.delete(handle);
915
+ }
916
+ }
917
+ class MainThreadTimer {
918
+ schedule(callback, delay) {
919
+ return window.setTimeout(callback, delay);
920
+ }
921
+ cancel(handle) {
922
+ window.clearTimeout(handle);
923
+ }
924
+ }
925
+ class DevTools {
926
+ constructor(options = {}) {
927
+ __publicField(this, "name", "DevTools");
928
+ __publicField(this, "options");
929
+ this.options = options;
930
+ }
931
+ init(scheduler) {
932
+ if (typeof window === "undefined" || typeof window.document === "undefined") {
933
+ return;
934
+ }
935
+ this.mount(scheduler);
936
+ }
937
+ async mount(scheduler) {
938
+ try {
939
+ await import("./devtools-ByJU-Gv1.js");
940
+ await customElements.whenDefined("hs-devtools");
941
+ this.setupElement(scheduler);
942
+ } catch (e) {
943
+ console.error("[DevTools] Failed to mount:", e);
944
+ }
945
+ }
946
+ setupElement(scheduler) {
947
+ try {
948
+ let el = document.querySelector("hs-devtools");
949
+ if (el) {
950
+ el.remove();
951
+ }
952
+ el = document.createElement("hs-devtools");
953
+ const options = this.options;
954
+ console.log("[DevTools Plugin] options:", JSON.stringify(options));
955
+ if (options.theme) el.setAttribute("theme", options.theme);
956
+ if (options.dockPosition) el.setAttribute("dock", options.dockPosition);
957
+ if (options.language) el.setAttribute("language", options.language);
958
+ if (options.defaultZoom) el.setAttribute("default-zoom", options.defaultZoom.toString());
959
+ if (options.trigger) {
960
+ if (options.trigger.backgroundColor) el.setAttribute("trigger-bg", options.trigger.backgroundColor);
961
+ if (options.trigger.textColor) el.setAttribute("trigger-color", options.trigger.textColor);
962
+ if (options.trigger.position) el.setAttribute("trigger-position", options.trigger.position);
963
+ }
964
+ console.log("[DevTools Plugin] attributes set:", el.getAttribute("dock"), el.getAttribute("trigger-position"));
965
+ document.body.appendChild(el);
966
+ if (typeof el.setScheduler === "function") {
967
+ el.setScheduler({
968
+ getTasks: () => {
969
+ return scheduler.getAllTasks().map((task) => {
970
+ var _a;
971
+ return {
972
+ id: task.id,
973
+ status: task.status,
974
+ lastRun: task.lastRun || null,
975
+ nextRun: task.nextRun || null,
976
+ executionCount: task.executionCount || 0,
977
+ schedule: task.schedule,
978
+ tags: task.tags || [],
979
+ error: task.status === TaskStatus.ERROR ? "Execution failed" : null,
980
+ driver: scheduler.getTaskDriver(task.id),
981
+ namespace: ((_a = task.options) == null ? void 0 : _a.namespace) || "default"
982
+ // 添加 namespace
983
+ };
984
+ });
985
+ },
986
+ on: (evt, handler) => {
987
+ return scheduler.on(evt, handler);
988
+ },
989
+ isRunning: () => scheduler.isRunning(),
990
+ trigger: (id) => scheduler.triggerTask(id),
991
+ pause: (id) => scheduler.stopTask(id),
992
+ resume: (id) => scheduler.startTask(id),
993
+ remove: (id) => scheduler.deleteTask(id)
994
+ });
995
+ } else {
996
+ console.warn("[DevTools] hs-devtools element does not have setScheduler method.");
997
+ }
998
+ } catch (e) {
999
+ console.error("[DevTools] Failed to setup element:", e);
1000
+ }
1001
+ }
1002
+ }
1003
+ const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
1004
+ let workerTimerInstance = null;
1005
+ let mainTimerInstance = null;
1006
+ function getDefaultTimerStrategy(config) {
1007
+ if (isBrowser) {
1008
+ const driver = (config == null ? void 0 : config.driver) || "worker";
1009
+ if (driver === "main") {
1010
+ if (!mainTimerInstance) mainTimerInstance = new MainThreadTimer();
1011
+ return mainTimerInstance;
1012
+ }
1013
+ if (!workerTimerInstance) workerTimerInstance = new BrowserTimer();
1014
+ return workerTimerInstance;
1015
+ }
1016
+ return new NodeTimer();
1017
+ }
1018
+ function createTimerStrategyFactory() {
1019
+ if (!isBrowser) return void 0;
1020
+ return (driver) => {
1021
+ if (driver === "main") {
1022
+ if (!mainTimerInstance) mainTimerInstance = new MainThreadTimer();
1023
+ return mainTimerInstance;
1024
+ }
1025
+ if (!workerTimerInstance) workerTimerInstance = new BrowserTimer();
1026
+ return workerTimerInstance;
1027
+ };
1028
+ }
1029
+ class Scheduler2 extends Scheduler$1 {
1030
+ /**
1031
+ * 创建调度器实例。
1032
+ * @param config 配置项
1033
+ */
1034
+ constructor(config) {
1035
+ super(
1036
+ getDefaultTimerStrategy(config),
1037
+ config,
1038
+ createTimerStrategyFactory()
1039
+ );
1040
+ }
1041
+ }
1042
+ export {
1043
+ Scheduler$1 as CoreScheduler,
1044
+ DevTools,
1045
+ Scheduler2 as Scheduler,
1046
+ SchedulerEvents,
1047
+ TaskStatus
1048
+ };