@h-ai/scheduler 0.1.0-alpha5

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/dist/index.js ADDED
@@ -0,0 +1,1416 @@
1
+ import { z } from 'zod';
2
+ import { cache } from '@h-ai/cache';
3
+ import { core, err, ok } from '@h-ai/core';
4
+ import { reldb, BaseReldbCrudRepository } from '@h-ai/reldb';
5
+ import { createHash } from 'crypto';
6
+ import { Script } from 'vm';
7
+ import { Cron } from 'croner';
8
+
9
+ // src/scheduler-config.ts
10
+ var SchedulerConfigSchema = z.object({
11
+ /** 是否启用数据库记录(默认 true,需要 @h-ai/reldb 已初始化) */
12
+ enableDb: z.boolean().default(true),
13
+ /** 调度检查间隔,单位毫秒(默认 1000,即每秒检查一次) */
14
+ tickInterval: z.number().int().min(100).default(1e3),
15
+ /** 分布式锁过期时间,单位毫秒(默认 300000,即 5 分钟) */
16
+ lockExpireMs: z.number().int().min(1e4).default(3e5),
17
+ /** 节点标识(默认自动生成 UUID,多节点部署时建议设置固定标识以便审计) */
18
+ nodeId: z.string().min(1).optional(),
19
+ /** 执行日志最大保留条数(启用 DB 时生效) */
20
+ maxLogs: z.number().int().min(1).optional(),
21
+ /** 执行日志保留天数(启用 DB 时生效) */
22
+ retentionDays: z.number().int().min(1).optional()
23
+ });
24
+
25
+ // messages/en-US.json
26
+ var en_US_default = {
27
+ $schema: "https://inlang.com/schema/inlang-message-format",
28
+ scheduler_notInitialized: "Scheduler not initialized, please call scheduler.init() first",
29
+ scheduler_initFailed: "Failed to initialize scheduler: {error}",
30
+ scheduler_alreadyRunning: "Scheduler is already running",
31
+ scheduler_notRunning: "Scheduler is not running",
32
+ scheduler_taskNotFound: "Task not found: {taskId}",
33
+ scheduler_taskAlreadyExists: "Task already exists: {taskId}",
34
+ scheduler_invalidCron: "Invalid cron expression: {expression}",
35
+ scheduler_executionFailed: "Task execution failed: {error}",
36
+ scheduler_jsExecutionFailed: "JS code execution failed: {error}",
37
+ scheduler_apiExecutionFailed: "API call failed: {error}",
38
+ scheduler_dbSaveFailed: "Failed to save execution log: {error}",
39
+ scheduler_dbNotInitialized: "Database not initialized, cannot save execution logs",
40
+ scheduler_configError: "Scheduler config validation failed: {error}",
41
+ scheduler_taskIdEmpty: "Task ID cannot be empty",
42
+ scheduler_taskRunning: "Task is currently running: {taskId}",
43
+ scheduler_invalidApiConfig: "Invalid api_config for task: {taskId}",
44
+ scheduler_lockAcquireFailed: "Failed to acquire distributed lock, another node already holds it: {taskId}",
45
+ scheduler_jsCompileFailed: "Failed to compile JS task: {error}",
46
+ scheduler_hookExecutionFailed: "Task lifecycle hook execution failed: {error}",
47
+ scheduler_taskHandlerMissing: "Task has no executable handler and no global execute hook is configured: {taskId}",
48
+ scheduler_invalidHandlerConfig: "Invalid task handler config: {taskId}",
49
+ scheduler_invalidRetryConfig: "Invalid retry policy config for task: {taskId}",
50
+ scheduler_reldbNotInitialized: "enableDb is true but @h-ai/reldb is not initialized, please call reldb.init() first"
51
+ };
52
+
53
+ // messages/zh-CN.json
54
+ var zh_CN_default = {
55
+ $schema: "https://inlang.com/schema/inlang-message-format",
56
+ scheduler_notInitialized: "\u5B9A\u65F6\u4EFB\u52A1\u6A21\u5757\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 scheduler.init()",
57
+ scheduler_initFailed: "\u5B9A\u65F6\u4EFB\u52A1\u6A21\u5757\u521D\u59CB\u5316\u5931\u8D25: {error}",
58
+ scheduler_alreadyRunning: "\u5B9A\u65F6\u4EFB\u52A1\u8C03\u5EA6\u5668\u5DF2\u5728\u8FD0\u884C\u4E2D",
59
+ scheduler_notRunning: "\u5B9A\u65F6\u4EFB\u52A1\u8C03\u5EA6\u5668\u672A\u8FD0\u884C",
60
+ scheduler_taskNotFound: "\u4EFB\u52A1\u672A\u627E\u5230: {taskId}",
61
+ scheduler_taskAlreadyExists: "\u4EFB\u52A1\u5DF2\u5B58\u5728: {taskId}",
62
+ scheduler_invalidCron: "\u65E0\u6548\u7684 cron \u8868\u8FBE\u5F0F: {expression}",
63
+ scheduler_executionFailed: "\u4EFB\u52A1\u6267\u884C\u5931\u8D25: {error}",
64
+ scheduler_jsExecutionFailed: "JS \u4EE3\u7801\u6267\u884C\u5931\u8D25: {error}",
65
+ scheduler_apiExecutionFailed: "API \u8C03\u7528\u5931\u8D25: {error}",
66
+ scheduler_dbSaveFailed: "\u4FDD\u5B58\u6267\u884C\u8BB0\u5F55\u5931\u8D25: {error}",
67
+ scheduler_dbNotInitialized: "\u6570\u636E\u5E93\u672A\u521D\u59CB\u5316\uFF0C\u65E0\u6CD5\u4FDD\u5B58\u6267\u884C\u8BB0\u5F55",
68
+ scheduler_configError: "\u8C03\u5EA6\u5668\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A{error}",
69
+ scheduler_taskIdEmpty: "\u4EFB\u52A1 ID \u4E0D\u80FD\u4E3A\u7A7A",
70
+ scheduler_taskRunning: "\u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D: {taskId}",
71
+ scheduler_invalidApiConfig: "\u4EFB\u52A1 api_config \u683C\u5F0F\u65E0\u6548: {taskId}",
72
+ scheduler_lockAcquireFailed: "\u83B7\u53D6\u5206\u5E03\u5F0F\u9501\u5931\u8D25\uFF0C\u5176\u4ED6\u8282\u70B9\u5DF2\u83B7\u9501: {taskId}",
73
+ scheduler_jsCompileFailed: "JS \u4EFB\u52A1\u7F16\u8BD1\u5931\u8D25: {error}",
74
+ scheduler_hookExecutionFailed: "\u4EFB\u52A1\u751F\u547D\u5468\u671F\u56DE\u8C03\u6267\u884C\u5931\u8D25: {error}",
75
+ scheduler_taskHandlerMissing: "\u4EFB\u52A1\u7F3A\u5C11\u53EF\u6267\u884C handler\uFF0C\u4E14\u672A\u914D\u7F6E\u5168\u5C40\u6267\u884C\u56DE\u8C03: {taskId}",
76
+ scheduler_invalidHandlerConfig: "\u4EFB\u52A1 handler \u914D\u7F6E\u65E0\u6548: {taskId}",
77
+ scheduler_invalidRetryConfig: "\u4EFB\u52A1\u91CD\u8BD5\u7B56\u7565\u914D\u7F6E\u65E0\u6548: {taskId}",
78
+ scheduler_reldbNotInitialized: "enableDb \u4E3A true \u4F46 @h-ai/reldb \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 reldb.init()"
79
+ };
80
+
81
+ // src/scheduler-i18n.ts
82
+ var schedulerM = core.i18n.createMessageGetter({
83
+ "zh-CN": zh_CN_default,
84
+ "en-US": en_US_default
85
+ });
86
+ var SchedulerErrorInfo = {
87
+ NOT_INITIALIZED: "010:500",
88
+ INIT_FAILED: "011:500",
89
+ CONFIG_ERROR: "012:500",
90
+ TASK_NOT_FOUND: "020:404",
91
+ TASK_ALREADY_EXISTS: "021:409",
92
+ INVALID_CRON: "022:400",
93
+ EXECUTION_FAILED: "023:500",
94
+ JS_EXECUTION_FAILED: "024:500",
95
+ API_EXECUTION_FAILED: "025:502",
96
+ DB_SAVE_FAILED: "026:500",
97
+ ALREADY_RUNNING: "027:409",
98
+ NOT_RUNNING: "028:400",
99
+ LOCK_ACQUIRE_FAILED: "029:409",
100
+ JS_COMPILE_FAILED: "030:500",
101
+ HOOK_EXECUTION_FAILED: "031:500"
102
+ };
103
+ var HaiSchedulerError = core.error.buildHaiErrorsDef("scheduler", SchedulerErrorInfo);
104
+
105
+ // src/repositories/scheduler-log-repository.ts
106
+ var logger = core.logger.child({ module: "scheduler", scope: "log-repository" });
107
+ var SCHEDULER_LOG_TABLE = "hai_scheduler_logs";
108
+ var SchedulerLogRepository = class extends BaseReldbCrudRepository {
109
+ constructor(db) {
110
+ super(db, {
111
+ table: SCHEDULER_LOG_TABLE,
112
+ idColumn: "id",
113
+ fields: [
114
+ { fieldName: "id", columnName: "id", def: { type: "INTEGER", primaryKey: true, autoIncrement: true }, select: true, create: false, update: false },
115
+ { fieldName: "taskId", columnName: "task_id", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
116
+ { fieldName: "taskName", columnName: "task_name", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
117
+ { fieldName: "taskType", columnName: "task_type", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
118
+ { fieldName: "triggerType", columnName: "trigger_type", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
119
+ { fieldName: "triggerSource", columnName: "trigger_source", def: { type: "TEXT" }, select: true, create: true, update: false },
120
+ { fieldName: "status", columnName: "status", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
121
+ { fieldName: "result", columnName: "result", def: { type: "TEXT" }, select: true, create: true, update: false },
122
+ { fieldName: "error", columnName: "error", def: { type: "TEXT" }, select: true, create: true, update: false },
123
+ { fieldName: "startedAt", columnName: "started_at", def: { type: "INTEGER", notNull: true }, select: true, create: true, update: false },
124
+ { fieldName: "finishedAt", columnName: "finished_at", def: { type: "INTEGER", notNull: true }, select: true, create: true, update: false },
125
+ { fieldName: "duration", columnName: "duration", def: { type: "INTEGER", notNull: true }, select: true, create: true, update: false }
126
+ ]
127
+ });
128
+ }
129
+ async saveLog(log) {
130
+ try {
131
+ const createResult = await this.create({
132
+ taskId: log.taskId,
133
+ taskName: log.taskName,
134
+ taskType: log.taskType,
135
+ triggerType: log.triggerType,
136
+ triggerSource: log.triggerSource,
137
+ status: log.status,
138
+ result: log.result,
139
+ error: log.error,
140
+ startedAt: log.startedAt,
141
+ finishedAt: log.finishedAt,
142
+ duration: log.duration
143
+ });
144
+ if (!createResult.success)
145
+ logger.error("Failed to save execution log", { taskId: log.taskId, error: createResult.error.message });
146
+ } catch (error) {
147
+ logger.error("Failed to save execution log", { taskId: log.taskId, error });
148
+ }
149
+ }
150
+ async queryLogs(options) {
151
+ const { taskId, status, triggerType, triggerSource, startedAfter, startedBefore, pagination } = options ?? {};
152
+ const conditions = [];
153
+ const params = [];
154
+ if (taskId) {
155
+ conditions.push("task_id = ?");
156
+ params.push(taskId);
157
+ }
158
+ if (status) {
159
+ conditions.push("status = ?");
160
+ params.push(status);
161
+ }
162
+ if (triggerType) {
163
+ conditions.push("trigger_type = ?");
164
+ params.push(triggerType);
165
+ }
166
+ if (triggerSource) {
167
+ conditions.push("trigger_source = ?");
168
+ params.push(triggerSource);
169
+ }
170
+ if (startedAfter !== void 0) {
171
+ conditions.push("started_at >= ?");
172
+ params.push(startedAfter);
173
+ }
174
+ if (startedBefore !== void 0) {
175
+ conditions.push("started_at <= ?");
176
+ params.push(startedBefore);
177
+ }
178
+ try {
179
+ const pageResult = await this.findPage({
180
+ where: conditions.length > 0 ? conditions.join(" AND ") : void 0,
181
+ params: params.length > 0 ? params : void 0,
182
+ orderBy: "id DESC",
183
+ pagination,
184
+ overrides: { defaultPageSize: 20, maxPageSize: 200 }
185
+ });
186
+ if (!pageResult.success) {
187
+ return err(
188
+ HaiSchedulerError.DB_SAVE_FAILED,
189
+ schedulerM("scheduler_dbSaveFailed", { params: { error: pageResult.error.message } }),
190
+ pageResult.error
191
+ );
192
+ }
193
+ return ok({
194
+ ...pageResult.data,
195
+ items: pageResult.data.items.map((row) => ({
196
+ id: row.id,
197
+ taskId: row.taskId,
198
+ taskName: row.taskName,
199
+ taskType: row.taskType,
200
+ triggerType: row.triggerType,
201
+ triggerSource: row.triggerSource ?? null,
202
+ status: row.status,
203
+ result: row.result ?? null,
204
+ error: row.error ?? null,
205
+ startedAt: row.startedAt,
206
+ finishedAt: row.finishedAt,
207
+ duration: row.duration
208
+ }))
209
+ });
210
+ } catch (error) {
211
+ logger.error("Failed to query execution logs", { error });
212
+ return err(
213
+ HaiSchedulerError.DB_SAVE_FAILED,
214
+ schedulerM("scheduler_dbSaveFailed", {
215
+ params: { error: error instanceof Error ? error.message : String(error) }
216
+ }),
217
+ error
218
+ );
219
+ }
220
+ }
221
+ async cleanupLogs(policy) {
222
+ const { maxLogs, retentionDays } = policy;
223
+ if (!maxLogs && !retentionDays)
224
+ return ok(void 0);
225
+ try {
226
+ if (retentionDays) {
227
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
228
+ const retentionResult = await this.sql().execute(
229
+ `DELETE FROM ${SCHEDULER_LOG_TABLE} WHERE started_at < ?`,
230
+ [cutoff]
231
+ );
232
+ if (!retentionResult.success) {
233
+ return err(
234
+ HaiSchedulerError.DB_SAVE_FAILED,
235
+ schedulerM("scheduler_dbSaveFailed", { params: { error: retentionResult.error.message } }),
236
+ retentionResult.error
237
+ );
238
+ }
239
+ }
240
+ if (maxLogs) {
241
+ const maxLogsResult = await this.sql().execute(
242
+ `DELETE FROM ${SCHEDULER_LOG_TABLE}
243
+ WHERE id NOT IN (
244
+ SELECT id FROM (
245
+ SELECT id
246
+ FROM ${SCHEDULER_LOG_TABLE}
247
+ ORDER BY started_at DESC, id DESC
248
+ LIMIT ?
249
+ ) AS keep_logs
250
+ )`,
251
+ [maxLogs]
252
+ );
253
+ if (!maxLogsResult.success) {
254
+ return err(
255
+ HaiSchedulerError.DB_SAVE_FAILED,
256
+ schedulerM("scheduler_dbSaveFailed", { params: { error: maxLogsResult.error.message } }),
257
+ maxLogsResult.error
258
+ );
259
+ }
260
+ }
261
+ return ok(void 0);
262
+ } catch (error) {
263
+ logger.error("Failed to cleanup execution logs", { error, maxLogs, retentionDays });
264
+ return err(
265
+ HaiSchedulerError.DB_SAVE_FAILED,
266
+ schedulerM("scheduler_dbSaveFailed", {
267
+ params: { error: error instanceof Error ? error.message : String(error) }
268
+ }),
269
+ error
270
+ );
271
+ }
272
+ }
273
+ };
274
+ var logger2 = core.logger.child({ module: "scheduler", scope: "task-repository" });
275
+ var SCHEDULER_TASK_TABLE = "hai_scheduler_tasks";
276
+ var TaskParamsSchema = z.record(z.string(), z.unknown());
277
+ var TaskRetryPolicySchema = z.object({
278
+ maxAttempts: z.number().int().min(1),
279
+ backoffMs: z.array(z.number().int().min(0)).optional()
280
+ });
281
+ var ApiTaskConfigSchema = z.object({
282
+ kind: z.literal("api"),
283
+ url: z.string().min(1),
284
+ method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional(),
285
+ headers: z.record(z.string(), z.string()).optional(),
286
+ body: z.unknown().optional(),
287
+ timeout: z.number().int().positive().optional()
288
+ });
289
+ var JsTaskConfigSchema = z.object({
290
+ kind: z.literal("js"),
291
+ code: z.string().min(1),
292
+ timeout: z.number().int().positive().optional()
293
+ });
294
+ var TaskHandlerConfigSchema = z.discriminatedUnion("kind", [ApiTaskConfigSchema, JsTaskConfigSchema]);
295
+ var SchedulerTaskRepository = class extends BaseReldbCrudRepository {
296
+ constructor(db) {
297
+ super(db, {
298
+ table: SCHEDULER_TASK_TABLE,
299
+ idColumn: "id",
300
+ fields: [
301
+ { fieldName: "id", columnName: "id", def: { type: "INTEGER", primaryKey: true, autoIncrement: true }, select: true, create: false, update: false },
302
+ { fieldName: "taskId", columnName: "task_id", def: { type: "TEXT", notNull: true, unique: true }, select: true, create: true, update: false },
303
+ { fieldName: "taskName", columnName: "task_name", def: { type: "TEXT", notNull: true }, select: true, create: true, update: true },
304
+ { fieldName: "description", columnName: "description", def: { type: "TEXT" }, select: true, create: true, update: true },
305
+ { fieldName: "cron", columnName: "cron", def: { type: "TEXT", notNull: true }, select: true, create: true, update: true },
306
+ { fieldName: "enabled", columnName: "enabled", def: { type: "BOOLEAN", notNull: true, defaultValue: true }, select: true, create: true, update: true },
307
+ { fieldName: "deleteAfterRun", columnName: "delete_after_run", def: { type: "BOOLEAN", notNull: true, defaultValue: false }, select: true, create: true, update: true },
308
+ { fieldName: "retry", columnName: "retry", def: { type: "JSON" }, select: true, create: true, update: true },
309
+ { fieldName: "params", columnName: "params", def: { type: "JSON" }, select: true, create: true, update: true },
310
+ { fieldName: "handler", columnName: "handler", def: { type: "JSON" }, select: true, create: true, update: true },
311
+ { fieldName: "createdAt", columnName: "created_at", def: { type: "TIMESTAMP" }, select: true, create: true, update: false },
312
+ { fieldName: "updatedAt", columnName: "updated_at", def: { type: "TIMESTAMP" }, select: true, create: false, update: true }
313
+ ]
314
+ });
315
+ }
316
+ async saveTask(task) {
317
+ try {
318
+ const createResult = await this.create({
319
+ taskId: task.id,
320
+ taskName: task.name,
321
+ description: task.description ?? null,
322
+ cron: task.cron,
323
+ enabled: task.enabled !== false,
324
+ deleteAfterRun: task.deleteAfterRun === true,
325
+ retry: task.retry ?? null,
326
+ params: task.params ?? {},
327
+ handler: task.handler ?? null,
328
+ createdAt: /* @__PURE__ */ new Date()
329
+ });
330
+ if (!createResult.success) {
331
+ logger2.error("Failed to save task definition", { taskId: task.id, error: createResult.error.message });
332
+ return err(
333
+ HaiSchedulerError.DB_SAVE_FAILED,
334
+ schedulerM("scheduler_dbSaveFailed", { params: { error: createResult.error.message } }),
335
+ createResult.error
336
+ );
337
+ }
338
+ return ok(void 0);
339
+ } catch (error) {
340
+ logger2.error("Failed to save task definition", { taskId: task.id, error });
341
+ return err(
342
+ HaiSchedulerError.DB_SAVE_FAILED,
343
+ schedulerM("scheduler_dbSaveFailed", {
344
+ params: { error: error instanceof Error ? error.message : String(error) }
345
+ }),
346
+ error
347
+ );
348
+ }
349
+ }
350
+ async updateTask(taskId, updates) {
351
+ try {
352
+ const rows = await this.findAll({ where: "task_id = ?", params: [taskId], limit: 1 });
353
+ if (!rows.success) {
354
+ return err(
355
+ HaiSchedulerError.DB_SAVE_FAILED,
356
+ schedulerM("scheduler_dbSaveFailed", { params: { error: rows.error.message } }),
357
+ rows.error
358
+ );
359
+ }
360
+ if (rows.data.length === 0)
361
+ return ok(void 0);
362
+ const data = {};
363
+ if (updates.name !== void 0)
364
+ data.taskName = updates.name;
365
+ if (updates.description !== void 0)
366
+ data.description = updates.description ?? null;
367
+ if (updates.cron !== void 0)
368
+ data.cron = updates.cron;
369
+ if (updates.enabled !== void 0)
370
+ data.enabled = updates.enabled;
371
+ if (updates.deleteAfterRun !== void 0)
372
+ data.deleteAfterRun = updates.deleteAfterRun;
373
+ if (updates.retry !== void 0)
374
+ data.retry = updates.retry ?? null;
375
+ if (updates.params !== void 0)
376
+ data.params = updates.params;
377
+ if (updates.handler !== void 0)
378
+ data.handler = updates.handler ?? null;
379
+ data.updatedAt = /* @__PURE__ */ new Date();
380
+ if (Object.keys(data).length <= 1)
381
+ return ok(void 0);
382
+ const updateResult = await this.updateById(rows.data[0].id, data);
383
+ if (!updateResult.success) {
384
+ logger2.error("Failed to update task definition", { taskId, error: updateResult.error.message });
385
+ return err(
386
+ HaiSchedulerError.DB_SAVE_FAILED,
387
+ schedulerM("scheduler_dbSaveFailed", { params: { error: updateResult.error.message } }),
388
+ updateResult.error
389
+ );
390
+ }
391
+ return ok(void 0);
392
+ } catch (error) {
393
+ logger2.error("Failed to update task definition", { taskId, error });
394
+ return err(
395
+ HaiSchedulerError.DB_SAVE_FAILED,
396
+ schedulerM("scheduler_dbSaveFailed", {
397
+ params: { error: error instanceof Error ? error.message : String(error) }
398
+ }),
399
+ error
400
+ );
401
+ }
402
+ }
403
+ async deleteTask(taskId) {
404
+ try {
405
+ const rows = await this.findAll({ where: "task_id = ?", params: [taskId], limit: 1 });
406
+ if (!rows.success) {
407
+ return err(
408
+ HaiSchedulerError.DB_SAVE_FAILED,
409
+ schedulerM("scheduler_dbSaveFailed", { params: { error: rows.error.message } }),
410
+ rows.error
411
+ );
412
+ }
413
+ if (rows.data.length === 0)
414
+ return ok(void 0);
415
+ const deleteResult = await this.deleteById(rows.data[0].id);
416
+ if (!deleteResult.success) {
417
+ logger2.error("Failed to delete task definition", { taskId, error: deleteResult.error.message });
418
+ return err(
419
+ HaiSchedulerError.DB_SAVE_FAILED,
420
+ schedulerM("scheduler_dbSaveFailed", { params: { error: deleteResult.error.message } }),
421
+ deleteResult.error
422
+ );
423
+ }
424
+ return ok(void 0);
425
+ } catch (error) {
426
+ logger2.error("Failed to delete task definition", { taskId, error });
427
+ return err(
428
+ HaiSchedulerError.DB_SAVE_FAILED,
429
+ schedulerM("scheduler_dbSaveFailed", {
430
+ params: { error: error instanceof Error ? error.message : String(error) }
431
+ }),
432
+ error
433
+ );
434
+ }
435
+ }
436
+ async loadTasks() {
437
+ try {
438
+ const rows = await this.findAll({ orderBy: "id ASC" });
439
+ if (!rows.success) {
440
+ return err(
441
+ HaiSchedulerError.DB_SAVE_FAILED,
442
+ schedulerM("scheduler_dbSaveFailed", { params: { error: rows.error.message } }),
443
+ rows.error
444
+ );
445
+ }
446
+ const tasks = [];
447
+ for (const row of rows.data) {
448
+ const paramsResult = TaskParamsSchema.safeParse(row.params ?? {});
449
+ if (!paramsResult.success) {
450
+ logger2.warn("Skipping task with invalid params", { taskId: row.taskId, error: paramsResult.error.message });
451
+ continue;
452
+ }
453
+ const handlerResult = row.handler == null ? { success: true, data: void 0 } : TaskHandlerConfigSchema.safeParse(row.handler);
454
+ if (!handlerResult.success) {
455
+ logger2.warn("Skipping task with invalid handler config", { taskId: row.taskId, error: handlerResult.error.message });
456
+ continue;
457
+ }
458
+ const retryResult = row.retry == null ? { success: true, data: void 0 } : TaskRetryPolicySchema.safeParse(row.retry);
459
+ if (!retryResult.success) {
460
+ logger2.warn("Skipping task with invalid retry policy", { taskId: row.taskId, error: retryResult.error.message });
461
+ continue;
462
+ }
463
+ tasks.push({
464
+ id: row.taskId,
465
+ name: row.taskName,
466
+ ...row.description ? { description: row.description } : {},
467
+ cron: row.cron,
468
+ enabled: row.enabled,
469
+ deleteAfterRun: row.deleteAfterRun,
470
+ ...retryResult.data ? { retry: retryResult.data } : {},
471
+ params: paramsResult.data,
472
+ ...handlerResult.data ? { handler: handlerResult.data } : {}
473
+ });
474
+ }
475
+ return ok(tasks);
476
+ } catch (error) {
477
+ logger2.error("Failed to load task definitions", { error });
478
+ return err(
479
+ HaiSchedulerError.DB_SAVE_FAILED,
480
+ schedulerM("scheduler_dbSaveFailed", {
481
+ params: { error: error instanceof Error ? error.message : String(error) }
482
+ }),
483
+ error
484
+ );
485
+ }
486
+ }
487
+ };
488
+ var compiledHandlerCache = /* @__PURE__ */ new Map();
489
+ function createCacheKey(config) {
490
+ const keySource = `${config.code}:${config.timeout ?? 0}`;
491
+ return createHash("sha256").update(keySource).digest("hex");
492
+ }
493
+ function clearJsTaskHandlerCache() {
494
+ compiledHandlerCache.clear();
495
+ }
496
+ function compileJsTaskHandler(config) {
497
+ const cacheKey = createCacheKey(config);
498
+ const cachedHandler = compiledHandlerCache.get(cacheKey);
499
+ if (cachedHandler)
500
+ return ok(cachedHandler);
501
+ try {
502
+ const script = new Script(`(${config.code})`);
503
+ const candidate = script.runInNewContext({});
504
+ if (typeof candidate !== "function") {
505
+ return err(
506
+ HaiSchedulerError.JS_COMPILE_FAILED,
507
+ schedulerM("scheduler_jsCompileFailed", { params: { error: "Compiled result is not a function" } })
508
+ );
509
+ }
510
+ const handler = async (context) => {
511
+ const functionResult = candidate(context);
512
+ if (!config.timeout || config.timeout <= 0)
513
+ return functionResult;
514
+ return await new Promise((resolve, reject) => {
515
+ const timer = setTimeout(() => {
516
+ reject(new Error(`JS task timed out after ${config.timeout}ms`));
517
+ }, config.timeout);
518
+ Promise.resolve(functionResult).then(
519
+ (result) => {
520
+ clearTimeout(timer);
521
+ resolve(result);
522
+ },
523
+ (error) => {
524
+ clearTimeout(timer);
525
+ reject(error);
526
+ }
527
+ );
528
+ });
529
+ };
530
+ compiledHandlerCache.set(cacheKey, handler);
531
+ return ok(handler);
532
+ } catch (error) {
533
+ return err(
534
+ HaiSchedulerError.JS_COMPILE_FAILED,
535
+ schedulerM("scheduler_jsCompileFailed", {
536
+ params: { error: error instanceof Error ? error.message : String(error) }
537
+ }),
538
+ error
539
+ );
540
+ }
541
+ }
542
+
543
+ // src/scheduler-executor.ts
544
+ var logger3 = core.logger.child({ module: "scheduler", scope: "executor" });
545
+ var currentLogRepo = null;
546
+ var currentCleanupPolicy = {};
547
+ function setLogRepository(repo, cleanupPolicy = {}) {
548
+ currentLogRepo = repo;
549
+ currentCleanupPolicy = { ...cleanupPolicy };
550
+ }
551
+ function sanitizeUrl(url) {
552
+ try {
553
+ const parsedUrl = new URL(url);
554
+ if (parsedUrl.password)
555
+ parsedUrl.password = "***";
556
+ if (parsedUrl.username)
557
+ parsedUrl.username = "***";
558
+ return parsedUrl.toString();
559
+ } catch {
560
+ return "(invalid url)";
561
+ }
562
+ }
563
+ function resolveTaskType(task, hooks) {
564
+ if (task.handler)
565
+ return task.handler.kind;
566
+ if (hooks.onTaskExecute)
567
+ return "hook";
568
+ return "hook";
569
+ }
570
+ function createTaskContext(task, trigger) {
571
+ return {
572
+ task,
573
+ taskId: task.id,
574
+ params: task.params ?? {},
575
+ trigger
576
+ };
577
+ }
578
+ async function notifyTaskStart(hooks, event) {
579
+ if (!hooks.onTaskStart)
580
+ return;
581
+ try {
582
+ await hooks.onTaskStart(event);
583
+ } catch (error) {
584
+ logger3.warn("Task start hook failed", { taskId: event.task.id, error });
585
+ }
586
+ }
587
+ async function notifyTaskInterrupted(hooks, event) {
588
+ if (!hooks.onTaskInterrupted)
589
+ return;
590
+ try {
591
+ await hooks.onTaskInterrupted(event);
592
+ } catch (error) {
593
+ logger3.warn("Task interrupted hook failed", { taskId: event.task.id, error });
594
+ }
595
+ }
596
+ async function notifyTaskFinish(hooks, event) {
597
+ if (!hooks.onTaskFinish)
598
+ return;
599
+ try {
600
+ await hooks.onTaskFinish(event);
601
+ } catch (error) {
602
+ logger3.warn("Task finish hook failed", { taskId: event.task.id, error });
603
+ }
604
+ }
605
+ async function saveExecutionLog(task, trigger, taskType, status, startedAt, finishedAt, result, error) {
606
+ const log = {
607
+ id: 0,
608
+ taskId: task.id,
609
+ taskName: task.name,
610
+ taskType,
611
+ triggerType: trigger.type,
612
+ triggerSource: trigger.source,
613
+ status,
614
+ result,
615
+ error,
616
+ startedAt,
617
+ finishedAt,
618
+ duration: finishedAt - startedAt
619
+ };
620
+ if (!currentLogRepo)
621
+ return log;
622
+ await currentLogRepo.saveLog(log);
623
+ const cleanupResult = await currentLogRepo.cleanupLogs(currentCleanupPolicy);
624
+ if (!cleanupResult.success) {
625
+ logger3.warn("Failed to cleanup execution logs", {
626
+ taskId: task.id,
627
+ error: cleanupResult.error.message,
628
+ maxLogs: currentCleanupPolicy.maxLogs,
629
+ retentionDays: currentCleanupPolicy.retentionDays
630
+ });
631
+ }
632
+ return log;
633
+ }
634
+ async function saveInterruptedTaskLog(task, trigger, reason, taskType, startedAt, finishedAt) {
635
+ return saveExecutionLog(task, trigger, taskType, "interrupted", startedAt, finishedAt, null, reason);
636
+ }
637
+ async function interruptTask(task, trigger, reason, hooks) {
638
+ const startedAt = Date.now();
639
+ const taskType = resolveTaskType(task, hooks);
640
+ await notifyTaskStart(hooks, { task, trigger, startedAt });
641
+ const finishedAt = Date.now();
642
+ const interruptedLog = await saveInterruptedTaskLog(task, trigger, reason, taskType, startedAt, finishedAt);
643
+ await notifyTaskInterrupted(hooks, {
644
+ task,
645
+ trigger,
646
+ startedAt,
647
+ interruptedAt: finishedAt,
648
+ reason
649
+ });
650
+ return interruptedLog;
651
+ }
652
+ async function executeJsTask(task, context) {
653
+ const handler = task.handler;
654
+ if (!handler || handler.kind !== "js") {
655
+ return err(
656
+ HaiSchedulerError.EXECUTION_FAILED,
657
+ schedulerM("scheduler_invalidHandlerConfig", { params: { taskId: task.id } })
658
+ );
659
+ }
660
+ const compileResult = compileJsTaskHandler(handler);
661
+ if (!compileResult.success)
662
+ return compileResult;
663
+ try {
664
+ const result = await compileResult.data(context);
665
+ return ok(result !== void 0 ? JSON.stringify(result) : null);
666
+ } catch (error) {
667
+ const message = error instanceof Error ? error.message : String(error);
668
+ logger3.error("JS task execution failed", { taskId: task.id, error: message });
669
+ return err(
670
+ HaiSchedulerError.JS_EXECUTION_FAILED,
671
+ schedulerM("scheduler_jsExecutionFailed", { params: { error: message } }),
672
+ error
673
+ );
674
+ }
675
+ }
676
+ async function executeApiTask(task, context) {
677
+ const handler = task.handler;
678
+ if (!handler || handler.kind !== "api") {
679
+ return err(
680
+ HaiSchedulerError.EXECUTION_FAILED,
681
+ schedulerM("scheduler_invalidHandlerConfig", { params: { taskId: task.id } })
682
+ );
683
+ }
684
+ const { url, method = "GET", headers, body, timeout = 3e4 } = handler;
685
+ const controller = new AbortController();
686
+ const timer = setTimeout(() => controller.abort(), timeout);
687
+ try {
688
+ const response = await fetch(url, {
689
+ method,
690
+ headers: {
691
+ ...headers,
692
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
693
+ },
694
+ signal: controller.signal,
695
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
696
+ });
697
+ clearTimeout(timer);
698
+ const responseText = await response.text();
699
+ if (!response.ok) {
700
+ logger3.error("API task returned non-OK status", {
701
+ taskId: context.taskId,
702
+ status: response.status,
703
+ url: sanitizeUrl(url)
704
+ });
705
+ return err(
706
+ HaiSchedulerError.API_EXECUTION_FAILED,
707
+ schedulerM("scheduler_apiExecutionFailed", {
708
+ params: { error: `HTTP ${response.status}: ${responseText.slice(0, 200)}` }
709
+ })
710
+ );
711
+ }
712
+ return ok(responseText || null);
713
+ } catch (error) {
714
+ clearTimeout(timer);
715
+ const message = error instanceof Error ? error.message : String(error);
716
+ logger3.error("API task execution failed", { taskId: context.taskId, error: message, url: sanitizeUrl(url) });
717
+ return err(
718
+ HaiSchedulerError.API_EXECUTION_FAILED,
719
+ schedulerM("scheduler_apiExecutionFailed", { params: { error: message } }),
720
+ error
721
+ );
722
+ }
723
+ }
724
+ async function executeHookTask(task, context, hooks, startedAt) {
725
+ if (!hooks.onTaskExecute) {
726
+ return err(
727
+ HaiSchedulerError.EXECUTION_FAILED,
728
+ schedulerM("scheduler_taskHandlerMissing", { params: { taskId: task.id } })
729
+ );
730
+ }
731
+ const executeEvent = {
732
+ task,
733
+ trigger: context.trigger,
734
+ startedAt,
735
+ context
736
+ };
737
+ try {
738
+ const result = await hooks.onTaskExecute(executeEvent);
739
+ return ok(result !== void 0 ? JSON.stringify(result) : null);
740
+ } catch (error) {
741
+ const message = error instanceof Error ? error.message : String(error);
742
+ logger3.error("Task execute hook failed", { taskId: task.id, error: message });
743
+ return err(
744
+ HaiSchedulerError.HOOK_EXECUTION_FAILED,
745
+ schedulerM("scheduler_hookExecutionFailed", { params: { error: message } }),
746
+ error
747
+ );
748
+ }
749
+ }
750
+ async function executeTask(task, trigger, hooks) {
751
+ const startedAt = Date.now();
752
+ const taskType = resolveTaskType(task, hooks);
753
+ const context = createTaskContext(task, trigger);
754
+ await notifyTaskStart(hooks, { task, trigger, startedAt });
755
+ let executionResult;
756
+ let shouldNotifyInterrupted = false;
757
+ if (!task.handler) {
758
+ executionResult = await executeHookTask(task, context, hooks, startedAt);
759
+ shouldNotifyInterrupted = !executionResult.success;
760
+ } else if (task.handler.kind === "api") {
761
+ executionResult = await executeApiTask(task, context);
762
+ } else {
763
+ executionResult = await executeJsTask(task, context);
764
+ }
765
+ const finishedAt = Date.now();
766
+ const status = executionResult.success ? "success" : shouldNotifyInterrupted ? "interrupted" : "failed";
767
+ const log = await saveExecutionLog(
768
+ task,
769
+ trigger,
770
+ taskType,
771
+ status,
772
+ startedAt,
773
+ finishedAt,
774
+ executionResult.success ? executionResult.data : null,
775
+ executionResult.success ? null : executionResult.error.message
776
+ );
777
+ if (!executionResult.success && shouldNotifyInterrupted) {
778
+ await notifyTaskInterrupted(hooks, {
779
+ task,
780
+ trigger,
781
+ startedAt,
782
+ interruptedAt: finishedAt,
783
+ reason: executionResult.error.message
784
+ });
785
+ }
786
+ if (status !== "interrupted")
787
+ await notifyTaskFinish(hooks, { task, trigger, startedAt, finishedAt, log });
788
+ return log;
789
+ }
790
+ function parseCronExpression(expression) {
791
+ try {
792
+ const cron = new Cron(expression, { legacyMode: false });
793
+ return ok(cron);
794
+ } catch {
795
+ return err(
796
+ HaiSchedulerError.INVALID_CRON,
797
+ schedulerM("scheduler_invalidCron", { params: { expression } })
798
+ );
799
+ }
800
+ }
801
+
802
+ // src/scheduler-functions.ts
803
+ var logger4 = core.logger.child({ module: "scheduler", scope: "functions" });
804
+ var taskRegistry = /* @__PURE__ */ new Map();
805
+ var cronCache = /* @__PURE__ */ new Map();
806
+ var currentHooks = {};
807
+ function getTaskRegistry() {
808
+ return taskRegistry;
809
+ }
810
+ function getTask(taskId) {
811
+ return taskRegistry.get(taskId);
812
+ }
813
+ function hasTask(taskId) {
814
+ return taskRegistry.has(taskId);
815
+ }
816
+ function setTask(taskId, task) {
817
+ taskRegistry.set(taskId, task);
818
+ }
819
+ function deleteTask(taskId) {
820
+ taskRegistry.delete(taskId);
821
+ }
822
+ function getCron(taskId) {
823
+ return cronCache.get(taskId);
824
+ }
825
+ function setCron(taskId, cron) {
826
+ cronCache.set(taskId, cron);
827
+ }
828
+ function deleteCron(taskId) {
829
+ cronCache.delete(taskId);
830
+ }
831
+ function setHooks(hooks) {
832
+ currentHooks = { ...hooks };
833
+ }
834
+ function clearHooks() {
835
+ currentHooks = {};
836
+ }
837
+ function getHooks() {
838
+ return currentHooks;
839
+ }
840
+ function resetTaskState() {
841
+ taskRegistry.clear();
842
+ cronCache.clear();
843
+ currentHooks = {};
844
+ }
845
+ function normalizeTaskDescription(description) {
846
+ if (description === void 0)
847
+ return void 0;
848
+ const trimmed = description.trim();
849
+ return trimmed.length > 0 ? trimmed : void 0;
850
+ }
851
+ function validateRetryPolicy(taskId, retry) {
852
+ if (!Number.isInteger(retry.maxAttempts) || retry.maxAttempts < 1) {
853
+ return err(
854
+ HaiSchedulerError.EXECUTION_FAILED,
855
+ schedulerM("scheduler_invalidRetryConfig", { params: { taskId } })
856
+ );
857
+ }
858
+ if (retry.backoffMs !== void 0) {
859
+ const hasInvalidBackoff = retry.backoffMs.some((ms) => !Number.isInteger(ms) || ms < 0);
860
+ if (hasInvalidBackoff) {
861
+ return err(
862
+ HaiSchedulerError.EXECUTION_FAILED,
863
+ schedulerM("scheduler_invalidRetryConfig", { params: { taskId } })
864
+ );
865
+ }
866
+ }
867
+ return ok({
868
+ maxAttempts: retry.maxAttempts,
869
+ ...retry.backoffMs ? { backoffMs: [...retry.backoffMs] } : {}
870
+ });
871
+ }
872
+ async function loadPersistedTasks(taskRepo2) {
873
+ if (!taskRepo2)
874
+ return;
875
+ const loadResult = await taskRepo2.loadTasks();
876
+ if (!loadResult.success) {
877
+ logger4.warn("Failed to load persisted tasks", { error: loadResult.error.message });
878
+ return;
879
+ }
880
+ let loadedCount = 0;
881
+ for (const task of loadResult.data) {
882
+ const registerResult = await registerTask(task);
883
+ if (registerResult.success) {
884
+ loadedCount++;
885
+ logger4.debug("Loaded persisted task", { taskId: task.id, taskName: task.name });
886
+ continue;
887
+ }
888
+ logger4.warn("Failed to load persisted task", { taskId: task.id, error: registerResult.error.message });
889
+ }
890
+ if (loadedCount > 0)
891
+ logger4.info("Loaded persisted tasks", { count: loadedCount });
892
+ }
893
+ async function loadConfigTasks(tasks) {
894
+ let loadedCount = 0;
895
+ for (const task of tasks) {
896
+ if (!task.id) {
897
+ logger4.warn("Skipping config task with empty id");
898
+ continue;
899
+ }
900
+ if (hasTask(task.id)) {
901
+ logger4.debug("Skipping config task, already registered from DB", { taskId: task.id });
902
+ continue;
903
+ }
904
+ const registerResult = await registerTask(task);
905
+ if (registerResult.success) {
906
+ loadedCount++;
907
+ logger4.debug("Loaded config task", { taskId: task.id, taskName: task.name });
908
+ continue;
909
+ }
910
+ logger4.warn("Failed to load config task", { taskId: task.id, error: registerResult.error.message });
911
+ }
912
+ if (loadedCount > 0)
913
+ logger4.info("Loaded config tasks", { count: loadedCount });
914
+ }
915
+ async function registerTask(task, taskRepo2) {
916
+ if (!task.id) {
917
+ return err(
918
+ HaiSchedulerError.EXECUTION_FAILED,
919
+ schedulerM("scheduler_taskIdEmpty")
920
+ );
921
+ }
922
+ if (hasTask(task.id)) {
923
+ return err(
924
+ HaiSchedulerError.TASK_ALREADY_EXISTS,
925
+ schedulerM("scheduler_taskAlreadyExists", { params: { taskId: task.id } })
926
+ );
927
+ }
928
+ const cronResult = parseCronExpression(task.cron);
929
+ if (!cronResult.success)
930
+ return cronResult;
931
+ const retryResult = task.retry ? validateRetryPolicy(task.id, task.retry) : ok(void 0);
932
+ if (!retryResult.success)
933
+ return retryResult;
934
+ const normalizedTask = {
935
+ ...task,
936
+ description: normalizeTaskDescription(task.description),
937
+ enabled: task.enabled !== false,
938
+ deleteAfterRun: task.deleteAfterRun === true,
939
+ retry: retryResult.data,
940
+ params: task.params ?? {}
941
+ };
942
+ setTask(task.id, normalizedTask);
943
+ setCron(task.id, cronResult.data);
944
+ if (taskRepo2) {
945
+ const saveResult = await taskRepo2.saveTask(normalizedTask);
946
+ if (!saveResult.success) {
947
+ logger4.warn("Failed to persist task definition, rolling back in-memory registration", { taskId: task.id, error: saveResult.error.message });
948
+ deleteTask(task.id);
949
+ deleteCron(task.id);
950
+ return err(
951
+ HaiSchedulerError.DB_SAVE_FAILED,
952
+ schedulerM("scheduler_dbSaveFailed", { params: { error: saveResult.error.message } }),
953
+ saveResult.error
954
+ );
955
+ }
956
+ }
957
+ return ok(void 0);
958
+ }
959
+ async function unregisterTask(taskId, taskRepo2) {
960
+ if (!hasTask(taskId)) {
961
+ return err(
962
+ HaiSchedulerError.TASK_NOT_FOUND,
963
+ schedulerM("scheduler_taskNotFound", { params: { taskId } })
964
+ );
965
+ }
966
+ deleteTask(taskId);
967
+ deleteCron(taskId);
968
+ if (taskRepo2) {
969
+ const deleteResult = await taskRepo2.deleteTask(taskId);
970
+ if (!deleteResult.success) {
971
+ logger4.warn("Failed to delete persisted task definition", { taskId, error: deleteResult.error.message });
972
+ return err(
973
+ HaiSchedulerError.DB_SAVE_FAILED,
974
+ schedulerM("scheduler_dbSaveFailed", { params: { error: deleteResult.error.message } }),
975
+ deleteResult.error
976
+ );
977
+ }
978
+ }
979
+ return ok(void 0);
980
+ }
981
+ async function updateRegisteredTask(taskId, updates, taskRepo2) {
982
+ const existingTask = getTask(taskId);
983
+ if (!existingTask) {
984
+ return err(
985
+ HaiSchedulerError.TASK_NOT_FOUND,
986
+ schedulerM("scheduler_taskNotFound", { params: { taskId } })
987
+ );
988
+ }
989
+ const existingCron = getCron(taskId);
990
+ if (updates.cron !== void 0) {
991
+ const cronResult = parseCronExpression(updates.cron);
992
+ if (!cronResult.success)
993
+ return cronResult;
994
+ setCron(taskId, cronResult.data);
995
+ }
996
+ let normalizedRetry;
997
+ if (updates.retry !== void 0) {
998
+ if (updates.retry === null) {
999
+ normalizedRetry = null;
1000
+ } else {
1001
+ const retryResult = validateRetryPolicy(taskId, updates.retry);
1002
+ if (!retryResult.success)
1003
+ return retryResult;
1004
+ normalizedRetry = retryResult.data;
1005
+ }
1006
+ }
1007
+ const normalizedDescription = updates.description !== void 0 ? normalizeTaskDescription(updates.description) : void 0;
1008
+ const normalizedUpdates = {
1009
+ ...updates,
1010
+ ...updates.description !== void 0 ? { description: normalizedDescription } : {},
1011
+ ...updates.retry !== void 0 ? { retry: normalizedRetry } : {}
1012
+ };
1013
+ const updatedTask = {
1014
+ ...existingTask,
1015
+ ...normalizedUpdates.name !== void 0 ? { name: normalizedUpdates.name } : {},
1016
+ ...normalizedUpdates.description !== void 0 ? { description: normalizedUpdates.description } : {},
1017
+ ...normalizedUpdates.cron !== void 0 ? { cron: normalizedUpdates.cron } : {},
1018
+ ...normalizedUpdates.enabled !== void 0 ? { enabled: normalizedUpdates.enabled } : {},
1019
+ ...normalizedUpdates.deleteAfterRun !== void 0 ? { deleteAfterRun: normalizedUpdates.deleteAfterRun } : {},
1020
+ ...normalizedUpdates.retry !== void 0 ? { retry: normalizedUpdates.retry ?? void 0 } : {},
1021
+ ...normalizedUpdates.params !== void 0 ? { params: normalizedUpdates.params } : {},
1022
+ ...normalizedUpdates.handler !== void 0 ? { handler: normalizedUpdates.handler ?? void 0 } : {}
1023
+ };
1024
+ setTask(taskId, updatedTask);
1025
+ if (taskRepo2) {
1026
+ const updateResult = await taskRepo2.updateTask(taskId, normalizedUpdates);
1027
+ if (!updateResult.success) {
1028
+ logger4.warn("Failed to update persisted task definition, rolling back in-memory update", { taskId, error: updateResult.error.message });
1029
+ setTask(taskId, existingTask);
1030
+ if (updates.cron !== void 0 && existingCron)
1031
+ setCron(taskId, existingCron);
1032
+ return err(
1033
+ HaiSchedulerError.DB_SAVE_FAILED,
1034
+ schedulerM("scheduler_dbSaveFailed", { params: { error: updateResult.error.message } }),
1035
+ updateResult.error
1036
+ );
1037
+ }
1038
+ }
1039
+ return ok(void 0);
1040
+ }
1041
+ async function queryTaskLogs(logRepo2, options) {
1042
+ if (!logRepo2) {
1043
+ return err(
1044
+ HaiSchedulerError.DB_SAVE_FAILED,
1045
+ schedulerM("scheduler_dbNotInitialized")
1046
+ );
1047
+ }
1048
+ return logRepo2.queryLogs(options);
1049
+ }
1050
+ var logger5 = core.logger.child({ module: "scheduler", scope: "runner" });
1051
+ var runningTasks = /* @__PURE__ */ new Set();
1052
+ var tickTimer = null;
1053
+ var lastTickMinute = -1;
1054
+ var currentNodeId = "";
1055
+ var currentLockTtlSec = 300;
1056
+ var currentTaskRepo = null;
1057
+ function configureLock(nodeId, lockTtlSec) {
1058
+ currentNodeId = nodeId;
1059
+ currentLockTtlSec = lockTtlSec;
1060
+ }
1061
+ function setTaskRepository(repo) {
1062
+ currentTaskRepo = repo;
1063
+ }
1064
+ function isTaskRunning(taskId) {
1065
+ return runningTasks.has(taskId);
1066
+ }
1067
+ function isTimerRunning() {
1068
+ return tickTimer !== null;
1069
+ }
1070
+ function resetRunner() {
1071
+ runningTasks.clear();
1072
+ if (tickTimer) {
1073
+ clearInterval(tickTimer);
1074
+ tickTimer = null;
1075
+ }
1076
+ lastTickMinute = -1;
1077
+ currentNodeId = "";
1078
+ currentLockTtlSec = 300;
1079
+ currentTaskRepo = null;
1080
+ }
1081
+ async function sleep(ms) {
1082
+ if (ms <= 0)
1083
+ return;
1084
+ await new Promise((resolve) => setTimeout(resolve, ms));
1085
+ }
1086
+ function resolveRetryBackoffMs(task, attempt) {
1087
+ const backoff = task.retry?.backoffMs;
1088
+ if (!backoff || backoff.length === 0)
1089
+ return 0;
1090
+ const index = Math.min(Math.max(attempt - 2, 0), backoff.length - 1);
1091
+ return backoff[index] ?? 0;
1092
+ }
1093
+ function tick() {
1094
+ const now = /* @__PURE__ */ new Date();
1095
+ const currentMinute = Math.floor(now.getTime() / 6e4);
1096
+ if (currentMinute === lastTickMinute)
1097
+ return;
1098
+ lastTickMinute = currentMinute;
1099
+ for (const [taskId, task] of getTaskRegistry()) {
1100
+ if (task.enabled === false)
1101
+ continue;
1102
+ const cron = getCron(taskId);
1103
+ if (!cron || !cron.match(now))
1104
+ continue;
1105
+ if (runningTasks.has(taskId)) {
1106
+ logger5.debug("Skipping task already running", { taskId, taskName: task.name });
1107
+ continue;
1108
+ }
1109
+ void runTask(task, currentMinute, { type: "scheduled", source: null }).catch((error) => {
1110
+ logger5.error("Unhandled task execution error", { taskId, error });
1111
+ });
1112
+ }
1113
+ }
1114
+ async function runTask(task, minuteTimestamp, trigger = { type: "manual", source: null }) {
1115
+ const lockKey = minuteTimestamp !== void 0 ? `hai:scheduler:${task.id}:${minuteTimestamp}` : void 0;
1116
+ if (cache.isInitialized && lockKey) {
1117
+ const lockResult = await cache.lock.acquire(lockKey, { ttl: currentLockTtlSec, owner: currentNodeId });
1118
+ if (!lockResult.success) {
1119
+ logger5.warn("Failed to acquire distributed lock, interrupting task", { taskId: task.id, error: lockResult.error.message });
1120
+ return interruptTask(
1121
+ task,
1122
+ trigger,
1123
+ schedulerM("scheduler_lockAcquireFailed", { params: { taskId: task.id } }),
1124
+ getHooks()
1125
+ );
1126
+ }
1127
+ if (!lockResult.data) {
1128
+ logger5.debug("Skipping task, another node holds the lock", { taskId: task.id, minuteTimestamp });
1129
+ return interruptTask(
1130
+ task,
1131
+ trigger,
1132
+ schedulerM("scheduler_lockAcquireFailed", { params: { taskId: task.id } }),
1133
+ getHooks()
1134
+ );
1135
+ }
1136
+ }
1137
+ if (runningTasks.has(task.id)) {
1138
+ return interruptTask(
1139
+ task,
1140
+ trigger,
1141
+ schedulerM("scheduler_taskRunning", { params: { taskId: task.id } }),
1142
+ getHooks()
1143
+ );
1144
+ }
1145
+ runningTasks.add(task.id);
1146
+ try {
1147
+ const maxAttempts = Math.max(1, task.retry?.maxAttempts ?? 1);
1148
+ let log = null;
1149
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1150
+ log = await executeTask(task, trigger, getHooks());
1151
+ if (log.status !== "failed")
1152
+ break;
1153
+ if (attempt >= maxAttempts)
1154
+ break;
1155
+ const backoffMs = resolveRetryBackoffMs(task, attempt + 1);
1156
+ logger5.warn("Retrying failed task execution", {
1157
+ taskId: task.id,
1158
+ attempt,
1159
+ maxAttempts,
1160
+ backoffMs
1161
+ });
1162
+ await sleep(backoffMs);
1163
+ }
1164
+ if (!log) {
1165
+ return interruptTask(
1166
+ task,
1167
+ trigger,
1168
+ schedulerM("scheduler_executionFailed", { params: { error: "Task execution produced no log" } }),
1169
+ getHooks()
1170
+ );
1171
+ }
1172
+ if (log.status === "failed")
1173
+ logger5.warn("Task execution failed", { taskId: task.id, error: log.error });
1174
+ else if (log.status === "interrupted")
1175
+ logger5.debug("Task execution interrupted", { taskId: task.id, error: log.error });
1176
+ else
1177
+ logger5.debug("Task execution succeeded", { taskId: task.id, duration: log.duration });
1178
+ if (task.deleteAfterRun === true) {
1179
+ const unregisterResult = await unregisterTask(task.id, currentTaskRepo);
1180
+ if (!unregisterResult.success) {
1181
+ logger5.warn("Failed to auto-delete one-time task after run", {
1182
+ taskId: task.id,
1183
+ error: unregisterResult.error.message
1184
+ });
1185
+ } else {
1186
+ logger5.info("Auto-deleted one-time task after run", { taskId: task.id });
1187
+ }
1188
+ }
1189
+ return log;
1190
+ } finally {
1191
+ runningTasks.delete(task.id);
1192
+ }
1193
+ }
1194
+ function startTimer(tickInterval) {
1195
+ lastTickMinute = -1;
1196
+ tickTimer = setInterval(() => tick(), tickInterval);
1197
+ }
1198
+ function stopTimer() {
1199
+ if (!tickTimer)
1200
+ return;
1201
+ clearInterval(tickTimer);
1202
+ tickTimer = null;
1203
+ }
1204
+
1205
+ // src/scheduler-main.ts
1206
+ var logger6 = core.logger.child({ module: "scheduler", scope: "main" });
1207
+ var currentConfig = null;
1208
+ var initInProgress = false;
1209
+ var taskRepo = null;
1210
+ var logRepo = null;
1211
+ var notInitialized = core.module.createNotInitializedKit(
1212
+ HaiSchedulerError.NOT_INITIALIZED,
1213
+ () => schedulerM("scheduler_notInitialized")
1214
+ );
1215
+ async function initDatabase(parsed) {
1216
+ if (!parsed.enableDb)
1217
+ return parsed;
1218
+ if (!reldb.isInitialized) {
1219
+ logger6.error("enableDb is true but @h-ai/reldb is not initialized");
1220
+ throw new Error(schedulerM("scheduler_reldbNotInitialized"));
1221
+ }
1222
+ taskRepo = new SchedulerTaskRepository(reldb);
1223
+ logRepo = new SchedulerLogRepository(reldb);
1224
+ setTaskRepository(taskRepo);
1225
+ setLogRepository(logRepo, {
1226
+ maxLogs: parsed.maxLogs,
1227
+ retentionDays: parsed.retentionDays
1228
+ });
1229
+ const nodeId = parsed.nodeId ?? crypto.randomUUID();
1230
+ const lockTtlSec = Math.ceil(parsed.lockExpireMs / 1e3);
1231
+ configureLock(nodeId, lockTtlSec);
1232
+ if (cache.isInitialized)
1233
+ logger6.info("Distributed lock configured (cache-based)", { nodeId, lockTtlSec });
1234
+ else
1235
+ logger6.warn("Cache module not initialized, distributed lock will be enabled when cache becomes available");
1236
+ return { ...parsed, nodeId };
1237
+ }
1238
+ function normalizeTriggerInput(options) {
1239
+ return {
1240
+ type: "manual",
1241
+ source: options?.source ?? null
1242
+ };
1243
+ }
1244
+ var scheduler = {
1245
+ async init(config) {
1246
+ if (initInProgress) {
1247
+ logger6.warn("Scheduler init already in progress, skipping concurrent call");
1248
+ return err(
1249
+ HaiSchedulerError.INIT_FAILED,
1250
+ schedulerM("scheduler_initFailed", { params: { error: "Concurrent initialization detected" } })
1251
+ );
1252
+ }
1253
+ initInProgress = true;
1254
+ try {
1255
+ if (currentConfig) {
1256
+ logger6.warn("Scheduler module is already initialized, reinitializing");
1257
+ await scheduler.close();
1258
+ }
1259
+ logger6.info("Initializing scheduler module");
1260
+ const { tasks = [], hooks = {}, ...configOptions } = config ?? {};
1261
+ const parseResult = SchedulerConfigSchema.safeParse(configOptions);
1262
+ if (!parseResult.success) {
1263
+ logger6.error("Scheduler config validation failed", { error: parseResult.error.message });
1264
+ return err(
1265
+ HaiSchedulerError.CONFIG_ERROR,
1266
+ schedulerM("scheduler_configError", { params: { error: parseResult.error.message } }),
1267
+ parseResult.error
1268
+ );
1269
+ }
1270
+ try {
1271
+ currentConfig = await initDatabase(parseResult.data);
1272
+ setHooks(hooks);
1273
+ await loadPersistedTasks(taskRepo);
1274
+ if (tasks.length > 0)
1275
+ await loadConfigTasks(tasks);
1276
+ logger6.info("Scheduler module initialized", { enableDb: currentConfig.enableDb, taskCount: getTaskRegistry().size });
1277
+ return ok(void 0);
1278
+ } catch (error) {
1279
+ logger6.error("Scheduler initialization failed", { error });
1280
+ await scheduler.close();
1281
+ return err(
1282
+ HaiSchedulerError.INIT_FAILED,
1283
+ schedulerM("scheduler_initFailed", {
1284
+ params: { error: error instanceof Error ? error.message : String(error) }
1285
+ }),
1286
+ error
1287
+ );
1288
+ }
1289
+ } finally {
1290
+ initInProgress = false;
1291
+ }
1292
+ },
1293
+ async register(task) {
1294
+ if (!currentConfig)
1295
+ return notInitialized.result();
1296
+ const registerResult = await registerTask(task, taskRepo);
1297
+ if (registerResult.success) {
1298
+ logger6.info("Task registered", { taskId: task.id, taskName: task.name, cron: task.cron });
1299
+ }
1300
+ return registerResult;
1301
+ },
1302
+ async unregister(taskId) {
1303
+ if (!currentConfig)
1304
+ return notInitialized.result();
1305
+ const unregisterResult = await unregisterTask(taskId, taskRepo);
1306
+ if (unregisterResult.success)
1307
+ logger6.info("Task unregistered", { taskId });
1308
+ return unregisterResult;
1309
+ },
1310
+ async updateTask(taskId, updates) {
1311
+ if (!currentConfig)
1312
+ return notInitialized.result();
1313
+ const updateResult = await updateRegisteredTask(taskId, updates, taskRepo);
1314
+ if (updateResult.success)
1315
+ logger6.info("Task updated", { taskId, updates: Object.keys(updates) });
1316
+ return updateResult;
1317
+ },
1318
+ start() {
1319
+ if (!currentConfig)
1320
+ return notInitialized.result();
1321
+ if (isTimerRunning()) {
1322
+ return err(
1323
+ HaiSchedulerError.ALREADY_RUNNING,
1324
+ schedulerM("scheduler_alreadyRunning")
1325
+ );
1326
+ }
1327
+ startTimer(currentConfig.tickInterval);
1328
+ logger6.info("Scheduler started", { tickInterval: currentConfig.tickInterval });
1329
+ return ok(void 0);
1330
+ },
1331
+ stop() {
1332
+ if (!currentConfig)
1333
+ return notInitialized.result();
1334
+ if (!isTimerRunning()) {
1335
+ return err(
1336
+ HaiSchedulerError.NOT_RUNNING,
1337
+ schedulerM("scheduler_notRunning")
1338
+ );
1339
+ }
1340
+ stopTimer();
1341
+ logger6.info("Scheduler stopped");
1342
+ return ok(void 0);
1343
+ },
1344
+ async trigger(taskId, options) {
1345
+ if (!currentConfig)
1346
+ return notInitialized.result();
1347
+ const task = getTask(taskId);
1348
+ if (!task) {
1349
+ return err(
1350
+ HaiSchedulerError.TASK_NOT_FOUND,
1351
+ schedulerM("scheduler_taskNotFound", { params: { taskId } })
1352
+ );
1353
+ }
1354
+ if (isTaskRunning(taskId)) {
1355
+ return err(
1356
+ HaiSchedulerError.EXECUTION_FAILED,
1357
+ schedulerM("scheduler_taskRunning", { params: { taskId } })
1358
+ );
1359
+ }
1360
+ const log = await runTask(task, void 0, normalizeTriggerInput(options));
1361
+ return ok(log);
1362
+ },
1363
+ async getLogs(options) {
1364
+ if (!currentConfig)
1365
+ return notInitialized.result();
1366
+ return queryTaskLogs(logRepo, options);
1367
+ },
1368
+ setHooks(hooks) {
1369
+ if (!currentConfig)
1370
+ return notInitialized.result();
1371
+ setHooks(hooks);
1372
+ return ok(void 0);
1373
+ },
1374
+ clearHooks() {
1375
+ if (!currentConfig)
1376
+ return notInitialized.result();
1377
+ clearHooks();
1378
+ return ok(void 0);
1379
+ },
1380
+ get tasks() {
1381
+ return getTaskRegistry();
1382
+ },
1383
+ get config() {
1384
+ return currentConfig;
1385
+ },
1386
+ get hooks() {
1387
+ return getHooks();
1388
+ },
1389
+ get isInitialized() {
1390
+ return currentConfig !== null;
1391
+ },
1392
+ get isRunning() {
1393
+ return isTimerRunning();
1394
+ },
1395
+ async close() {
1396
+ if (!currentConfig) {
1397
+ logger6.info("Scheduler module already closed, skipping");
1398
+ return;
1399
+ }
1400
+ logger6.info("Closing scheduler module");
1401
+ stopTimer();
1402
+ clearJsTaskHandlerCache();
1403
+ resetTaskState();
1404
+ resetRunner();
1405
+ setLogRepository(null);
1406
+ setTaskRepository(null);
1407
+ currentConfig = null;
1408
+ taskRepo = null;
1409
+ logRepo = null;
1410
+ logger6.info("Scheduler module closed");
1411
+ }
1412
+ };
1413
+
1414
+ export { HaiSchedulerError, SchedulerConfigSchema, scheduler };
1415
+ //# sourceMappingURL=index.js.map
1416
+ //# sourceMappingURL=index.js.map