@h-ai/scheduler 0.1.0-alpha.67 → 0.1.0-alpha.68
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/README.en.md +60 -34
- package/README.ja.md +44 -18
- package/README.md +60 -34
- package/dist/index.d.ts +528 -51
- package/dist/index.js +1125 -677
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -16,25 +16,106 @@ import { PaginationOptionsInput, HaiResult, PaginatedResult } from '@h-ai/core';
|
|
|
16
16
|
*
|
|
17
17
|
* @example
|
|
18
18
|
* ```ts
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* import { SchedulerConfigSchema } from '@h-ai/scheduler'
|
|
20
|
+
*
|
|
21
|
+
* const parsed = SchedulerConfigSchema.safeParse({ enableDb: true, tickInterval: 1000, leaseMs: 300000 })
|
|
22
|
+
* if (parsed.success) {
|
|
23
|
+
* const renewalPeriod = Math.floor(parsed.data.leaseMs / 3)
|
|
24
|
+
* }
|
|
23
25
|
* ```
|
|
24
26
|
*/
|
|
25
27
|
declare const SchedulerConfigSchema: z.ZodObject<{
|
|
26
|
-
enableDb: z.ZodDefault<z.
|
|
28
|
+
enableDb: z.ZodDefault<z.ZodLiteral<true>>;
|
|
27
29
|
tickInterval: z.ZodDefault<z.ZodNumber>;
|
|
28
|
-
|
|
30
|
+
leaseMs: z.ZodDefault<z.ZodNumber>;
|
|
29
31
|
nodeId: z.ZodOptional<z.ZodString>;
|
|
30
32
|
maxLogs: z.ZodOptional<z.ZodNumber>;
|
|
31
33
|
retentionDays: z.ZodOptional<z.ZodNumber>;
|
|
32
34
|
}, z.core.$strip>;
|
|
33
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* 手动触发边界,重复使用同一幂等键会命中同一个执行记录。
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { TriggerTaskSchema } from '@h-ai/scheduler'
|
|
41
|
+
*
|
|
42
|
+
* const parsed = TriggerTaskSchema.safeParse({ source: 'billing-console', idempotencyKey: 'billing-2026-10' })
|
|
43
|
+
* if (parsed.success) {
|
|
44
|
+
* const sameOccurrenceKey = parsed.data.idempotencyKey
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare const TriggerTaskSchema: z.ZodObject<{
|
|
49
|
+
source: z.ZodOptional<z.ZodString>;
|
|
50
|
+
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
51
|
+
}, z.core.$strip>;
|
|
52
|
+
/**
|
|
53
|
+
* 可信服务端人工核对结果;必须先停止旧 runner,不能将未知执行直接重排。
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { ExecutionResolutionSchema, scheduler } from '@h-ai/scheduler'
|
|
58
|
+
*
|
|
59
|
+
* // 可信运维流程已停止旧 runner 并核对实际外部结果。
|
|
60
|
+
* const pending = await scheduler.execution.listUnknown({ limit: 10 })
|
|
61
|
+
* if (!pending.success) throw new Error(pending.error.message)
|
|
62
|
+
* const entry = pending.data[0]
|
|
63
|
+
* if (entry) {
|
|
64
|
+
* const resolution = ExecutionResolutionSchema.parse({ status: 'discarded', expectedUpdatedAt: entry.updatedAt })
|
|
65
|
+
* const resolved = await scheduler.execution.resolveUnknown(entry.occurrenceId, resolution)
|
|
66
|
+
* if (!resolved.success) throw new Error(resolved.error.message)
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
declare const ExecutionResolutionSchema: z.ZodObject<{
|
|
71
|
+
status: z.ZodEnum<{
|
|
72
|
+
success: "success";
|
|
73
|
+
discarded: "discarded";
|
|
74
|
+
}>;
|
|
75
|
+
expectedUpdatedAt: z.ZodNumber;
|
|
76
|
+
}, z.core.$strip>;
|
|
77
|
+
/**
|
|
78
|
+
* 调度器配置类型
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* import type { SchedulerConfig } from '@h-ai/scheduler'
|
|
83
|
+
* import { SchedulerConfigSchema } from '@h-ai/scheduler'
|
|
84
|
+
*
|
|
85
|
+
* const config: SchedulerConfig = SchedulerConfigSchema.parse({ leaseMs: 60000 })
|
|
86
|
+
* const renewalInterval = config.leaseMs / 3
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
34
89
|
type SchedulerConfig = z.infer<typeof SchedulerConfigSchema>;
|
|
35
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* 调度器配置输入类型
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* import type { SchedulerConfigInput } from '@h-ai/scheduler'
|
|
96
|
+
* import { scheduler } from '@h-ai/scheduler'
|
|
97
|
+
*
|
|
98
|
+
* // 宿主已成功初始化共享 reldb。
|
|
99
|
+
* const config: SchedulerConfigInput = { leaseMs: 60000, retentionDays: 30 }
|
|
100
|
+
* const initialized = await scheduler.init(config)
|
|
101
|
+
* if (!initialized.success) throw new Error(initialized.error.message)
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
36
104
|
type SchedulerConfigInput = z.input<typeof SchedulerConfigSchema>;
|
|
37
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Scheduler 领域错误定义;业务调用先判断 success 再按错误码分支。
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* import { HaiSchedulerError, scheduler } from '@h-ai/scheduler'
|
|
112
|
+
*
|
|
113
|
+
* const result = await scheduler.trigger('missing-task')
|
|
114
|
+
* if (!result.success && result.error.code === HaiSchedulerError.TASK_NOT_FOUND.code) {
|
|
115
|
+
* const missingTask = true
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
38
119
|
declare const HaiSchedulerError: {
|
|
39
120
|
readonly NOT_INITIALIZED: _h_ai_core.HaiErrorDef;
|
|
40
121
|
readonly INIT_FAILED: _h_ai_core.HaiErrorDef;
|
|
@@ -48,27 +129,85 @@ declare const HaiSchedulerError: {
|
|
|
48
129
|
readonly DB_SAVE_FAILED: _h_ai_core.HaiErrorDef;
|
|
49
130
|
readonly ALREADY_RUNNING: _h_ai_core.HaiErrorDef;
|
|
50
131
|
readonly NOT_RUNNING: _h_ai_core.HaiErrorDef;
|
|
51
|
-
readonly LOCK_ACQUIRE_FAILED: _h_ai_core.HaiErrorDef;
|
|
52
132
|
readonly JS_COMPILE_FAILED: _h_ai_core.HaiErrorDef;
|
|
53
133
|
readonly HOOK_EXECUTION_FAILED: _h_ai_core.HaiErrorDef;
|
|
134
|
+
readonly LOG_PERSIST_FAILED: _h_ai_core.HaiErrorDef;
|
|
54
135
|
};
|
|
55
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* 任务参数字典(键值对)
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* import type { TaskParams } from '@h-ai/scheduler'
|
|
142
|
+
*
|
|
143
|
+
* const params: TaskParams = { month: '2026-10', dryRun: true }
|
|
144
|
+
* const month = params.month
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
56
147
|
type TaskParams = Record<string, unknown>;
|
|
57
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* 任务触发类型
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* import type { TaskTriggerType } from '@h-ai/scheduler'
|
|
154
|
+
*
|
|
155
|
+
* const triggerType: TaskTriggerType = 'manual'
|
|
156
|
+
* const isManual = triggerType === 'manual'
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
58
159
|
type TaskTriggerType = 'scheduled' | 'manual';
|
|
59
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* 任务处理器类型
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* import type { TaskHandlerKind } from '@h-ai/scheduler'
|
|
166
|
+
*
|
|
167
|
+
* const kind: TaskHandlerKind = 'api'
|
|
168
|
+
* const needsNetwork = kind === 'api'
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
60
171
|
type TaskHandlerKind = 'api' | 'js';
|
|
61
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* 失败重试策略
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* import type { TaskRetryPolicy } from '@h-ai/scheduler'
|
|
178
|
+
*
|
|
179
|
+
* const retry: TaskRetryPolicy = { maxAttempts: 3, backoffMs: [1000, 5000] }
|
|
180
|
+
* // 只有外部副作用确实按 occurrenceId 持久去重时,任务才能声明 idempotent。
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
62
183
|
interface TaskRetryPolicy {
|
|
63
|
-
/**
|
|
184
|
+
/** 最大尝试次数(含首次及崩溃恢复,范围 1–100,默认 1)。 */
|
|
64
185
|
maxAttempts: number;
|
|
65
186
|
/** 每次重试前的退避时间(毫秒);按顺序对应第 2、3... 次尝试 */
|
|
66
187
|
backoffMs?: number[];
|
|
67
188
|
}
|
|
68
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* 任务执行目标类型
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* import type { TaskExecutionTargetType } from '@h-ai/scheduler'
|
|
195
|
+
*
|
|
196
|
+
* const target: TaskExecutionTargetType = 'hook'
|
|
197
|
+
* const usesHostCallback = target === 'hook'
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
69
200
|
type TaskExecutionTargetType = TaskHandlerKind | 'hook';
|
|
70
201
|
/**
|
|
71
202
|
* API 任务配置
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```ts
|
|
206
|
+
* import type { ApiTaskConfig } from '@h-ai/scheduler'
|
|
207
|
+
*
|
|
208
|
+
* const handler: ApiTaskConfig = { kind: 'api', url: 'https://example.com/health', method: 'GET', timeout: 5000, maxResponseBytes: 4096 }
|
|
209
|
+
* // API 接收端若支持重试,须持久处理 Idempotency-Key 与 fencing 元数据。
|
|
210
|
+
* ```
|
|
72
211
|
*/
|
|
73
212
|
interface ApiTaskConfig {
|
|
74
213
|
/** 处理器类型 */
|
|
@@ -95,6 +234,14 @@ interface ApiTaskConfig {
|
|
|
95
234
|
*
|
|
96
235
|
* `code` 为 JS 函数字符串,例如:
|
|
97
236
|
* `(context) => ({ taskId: context.task.id, params: context.params })`
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```ts
|
|
240
|
+
* import type { JsTaskConfig } from '@h-ai/scheduler'
|
|
241
|
+
*
|
|
242
|
+
* const handler: JsTaskConfig = { kind: 'js', code: '(context) => ({ taskId: context.taskId })', timeout: 1000 }
|
|
243
|
+
* // 仅装载受信任的服务端源码;工作线程不是不可信代码沙箱。
|
|
244
|
+
* ```
|
|
98
245
|
*/
|
|
99
246
|
interface JsTaskConfig {
|
|
100
247
|
/** 处理器类型 */
|
|
@@ -104,34 +251,30 @@ interface JsTaskConfig {
|
|
|
104
251
|
/** 独立线程执行期限,单位毫秒(默认 30000,含线程启动);超时终止线程后才允许重试 */
|
|
105
252
|
timeout?: number;
|
|
106
253
|
}
|
|
107
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* 统一任务处理器配置
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```ts
|
|
259
|
+
* import type { TaskHandlerConfig } from '@h-ai/scheduler'
|
|
260
|
+
*
|
|
261
|
+
* const handler: TaskHandlerConfig = { kind: 'api', url: 'https://example.com/health' }
|
|
262
|
+
* const timeout = handler.timeout ?? 30000
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
108
265
|
type TaskHandlerConfig = ApiTaskConfig | JsTaskConfig;
|
|
109
266
|
/**
|
|
110
267
|
* 任务定义
|
|
111
268
|
*
|
|
112
269
|
* @example
|
|
113
270
|
* ```ts
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* name: '清理过期数据',
|
|
117
|
-
* cron: '0 2 * * *',
|
|
118
|
-
* params: { channel: 'nightly' },
|
|
119
|
-
* handler: {
|
|
120
|
-
* kind: 'js',
|
|
121
|
-
* code: '(context) => ({ taskId: context.task.id, params: context.params })',
|
|
122
|
-
* },
|
|
123
|
-
* }
|
|
271
|
+
* import type { TaskDefinition } from '@h-ai/scheduler'
|
|
272
|
+
* import { scheduler } from '@h-ai/scheduler'
|
|
124
273
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* handler: {
|
|
130
|
-
* kind: 'api',
|
|
131
|
-
* url: 'https://api.example.com/health',
|
|
132
|
-
* method: 'GET',
|
|
133
|
-
* },
|
|
134
|
-
* }
|
|
274
|
+
* // 宿主已成功初始化共享 reldb 与 scheduler。
|
|
275
|
+
* const definition: TaskDefinition = { ...{ id: 'daily-report', name: '每日报表', cron: '0 2 * * *', timezone: 'UTC' }, handler: { kind: 'api', url: 'https://example.com/health', maxResponseBytes: 4096 } }
|
|
276
|
+
* const registered = await scheduler.register(definition)
|
|
277
|
+
* if (!registered.success) throw new Error(registered.error.message)
|
|
135
278
|
* ```
|
|
136
279
|
*/
|
|
137
280
|
interface TaskDefinition {
|
|
@@ -143,7 +286,7 @@ interface TaskDefinition {
|
|
|
143
286
|
description?: string;
|
|
144
287
|
/** cron 表达式(标准 5 字段:分 时 日 月 周) */
|
|
145
288
|
cron: string;
|
|
146
|
-
/** IANA 时区,用于解释 cron
|
|
289
|
+
/** IANA 时区,用于解释 cron 墙上时间;省略或清空时统一使用 UTC,不读取节点 TZ。 */
|
|
147
290
|
timezone?: string;
|
|
148
291
|
/** 是否启用(默认 true) */
|
|
149
292
|
enabled?: boolean;
|
|
@@ -151,6 +294,8 @@ interface TaskDefinition {
|
|
|
151
294
|
deleteAfterRun?: boolean;
|
|
152
295
|
/** 失败重试策略 */
|
|
153
296
|
retry?: TaskRetryPolicy;
|
|
297
|
+
/** 全部副作用按 context.occurrenceId 原子持久去重时才为 true;默认 false,未知结果禁止自动重跑。 */
|
|
298
|
+
idempotent?: boolean;
|
|
154
299
|
/** 通用参数 */
|
|
155
300
|
params?: TaskParams;
|
|
156
301
|
/** 任务处理器;为空时可由全局事件回调统一处理 */
|
|
@@ -158,6 +303,14 @@ interface TaskDefinition {
|
|
|
158
303
|
}
|
|
159
304
|
/**
|
|
160
305
|
* 任务触发信息
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```ts
|
|
309
|
+
* import type { TaskTriggerInfo } from '@h-ai/scheduler'
|
|
310
|
+
*
|
|
311
|
+
* const trigger: TaskTriggerInfo = { type: 'manual', source: 'billing-console' }
|
|
312
|
+
* const source = trigger.source ?? 'cron'
|
|
313
|
+
* ```
|
|
161
314
|
*/
|
|
162
315
|
interface TaskTriggerInfo {
|
|
163
316
|
/** 触发类型 */
|
|
@@ -167,10 +320,27 @@ interface TaskTriggerInfo {
|
|
|
167
320
|
}
|
|
168
321
|
/**
|
|
169
322
|
* 任务执行上下文
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```ts
|
|
326
|
+
* import type { SchedulerTaskContext } from '@h-ai/scheduler'
|
|
327
|
+
*
|
|
328
|
+
* function safeReceipt(context: SchedulerTaskContext): string {
|
|
329
|
+
* if (!context.occurrenceId || !context.executionToken) throw new Error('Shared execution identity required')
|
|
330
|
+
* return context.occurrenceId
|
|
331
|
+
* }
|
|
332
|
+
* // 接收端将稳定 receipt 原子持久去重;executionToken 不写入用户可见日志。
|
|
333
|
+
* ```
|
|
170
334
|
*/
|
|
171
335
|
interface SchedulerTaskContext {
|
|
172
336
|
/** 一次调度发生的稳定标识;所有重试共享,供宿主构造业务幂等键。 */
|
|
173
337
|
occurrenceId?: string;
|
|
338
|
+
/** 当前领取独有的凭据,用于宿主条件提交;续租失败后不能继续产生副作用。 */
|
|
339
|
+
executionToken?: string;
|
|
340
|
+
/** 数据库执行序号;与 attempt 组成接收端可比较的 fencing 版本。 */
|
|
341
|
+
sequence?: number;
|
|
342
|
+
/** 同一 occurrence 中持久化的尝试次数。 */
|
|
343
|
+
attempt?: number;
|
|
174
344
|
/** 当前任务 */
|
|
175
345
|
task: TaskDefinition;
|
|
176
346
|
/** 任务 ID */
|
|
@@ -180,10 +350,35 @@ interface SchedulerTaskContext {
|
|
|
180
350
|
/** 触发信息 */
|
|
181
351
|
trigger: TaskTriggerInfo;
|
|
182
352
|
}
|
|
183
|
-
/**
|
|
353
|
+
/**
|
|
354
|
+
* 编译后的 JS 任务函数
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* ```ts
|
|
358
|
+
* import type { JsTaskHandler } from '@h-ai/scheduler'
|
|
359
|
+
*
|
|
360
|
+
* const handler: JsTaskHandler = (context, signal) => {
|
|
361
|
+
* signal?.throwIfAborted()
|
|
362
|
+
* return { taskId: context.taskId, batch: context.params.batch }
|
|
363
|
+
* }
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
184
366
|
type JsTaskHandler = (context: SchedulerTaskContext, signal?: AbortSignal) => unknown | Promise<unknown>;
|
|
185
|
-
/**
|
|
367
|
+
/**
|
|
368
|
+
* 任务开始事件
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* ```ts
|
|
372
|
+
* import type { SchedulerTaskStartEvent } from '@h-ai/scheduler'
|
|
373
|
+
*
|
|
374
|
+
* function summarizeStart(event: SchedulerTaskStartEvent) {
|
|
375
|
+
* return { taskId: event.task.id, occurrenceId: event.context?.occurrenceId, observedAt: event.startedAt }
|
|
376
|
+
* }
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
186
379
|
interface SchedulerTaskStartEvent {
|
|
380
|
+
/** 已领取执行的持久身份,观测回调也可据此标记或幂等处理;竞争中断时不存在。 */
|
|
381
|
+
context?: SchedulerTaskContext;
|
|
187
382
|
/** 关闭调度器时取消;回调必须停止后续副作用并释放资源 */
|
|
188
383
|
signal: AbortSignal;
|
|
189
384
|
/** 当前任务 */
|
|
@@ -193,19 +388,55 @@ interface SchedulerTaskStartEvent {
|
|
|
193
388
|
/** 开始时间(Unix 时间戳,毫秒) */
|
|
194
389
|
startedAt: number;
|
|
195
390
|
}
|
|
196
|
-
/**
|
|
391
|
+
/**
|
|
392
|
+
* 任务执行事件
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* ```ts
|
|
396
|
+
* import type { SchedulerTaskExecuteEvent } from '@h-ai/scheduler'
|
|
397
|
+
*
|
|
398
|
+
* function execute(event: SchedulerTaskExecuteEvent) {
|
|
399
|
+
* event.signal.throwIfAborted()
|
|
400
|
+
* return { taskId: event.context.taskId, params: event.context.params }
|
|
401
|
+
* }
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
197
404
|
interface SchedulerTaskExecuteEvent extends SchedulerTaskStartEvent {
|
|
198
405
|
/** 执行上下文 */
|
|
199
406
|
context: SchedulerTaskContext;
|
|
200
407
|
}
|
|
201
|
-
/**
|
|
408
|
+
/**
|
|
409
|
+
* 任务中断事件
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* ```ts
|
|
413
|
+
* import type { SchedulerTaskInterruptedEvent } from '@h-ai/scheduler'
|
|
414
|
+
*
|
|
415
|
+
* function summarizeInterruption(event: SchedulerTaskInterruptedEvent) {
|
|
416
|
+
* return { taskId: event.task.id, reason: event.reason, observedAt: event.interruptedAt }
|
|
417
|
+
* }
|
|
418
|
+
* // 中断观测不能证明外部副作用不存在。
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
202
421
|
interface SchedulerTaskInterruptedEvent extends SchedulerTaskStartEvent {
|
|
203
422
|
/** 中断时间(Unix 时间戳,毫秒) */
|
|
204
423
|
interruptedAt: number;
|
|
205
424
|
/** 中断原因 */
|
|
206
425
|
reason: string;
|
|
207
426
|
}
|
|
208
|
-
/**
|
|
427
|
+
/**
|
|
428
|
+
* 任务结束事件
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```ts
|
|
432
|
+
* import type { SchedulerTaskFinishEvent } from '@h-ai/scheduler'
|
|
433
|
+
*
|
|
434
|
+
* function needsLogRepair(event: SchedulerTaskFinishEvent): boolean {
|
|
435
|
+
* return event.log.persistence.status === 'pending'
|
|
436
|
+
* }
|
|
437
|
+
* // 结束回调是观测;业务是否已提交以 journal 为准。
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
209
440
|
interface SchedulerTaskFinishEvent extends SchedulerTaskStartEvent {
|
|
210
441
|
/** 结束时间(Unix 时间戳,毫秒) */
|
|
211
442
|
finishedAt: number;
|
|
@@ -217,6 +448,19 @@ interface SchedulerTaskFinishEvent extends SchedulerTaskStartEvent {
|
|
|
217
448
|
*
|
|
218
449
|
* - `onTaskExecute` 可用于统一处理无内置 handler 的任务
|
|
219
450
|
* - `onTaskStart` / `onTaskInterrupted` / `onTaskFinish` 用于观测生命周期
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* ```ts
|
|
454
|
+
* import type { SchedulerTaskHooks } from '@h-ai/scheduler'
|
|
455
|
+
*
|
|
456
|
+
* const hooks: SchedulerTaskHooks = {
|
|
457
|
+
* onTaskExecute: ({ context, signal }) => {
|
|
458
|
+
* signal.throwIfAborted()
|
|
459
|
+
* return { taskId: context.taskId }
|
|
460
|
+
* },
|
|
461
|
+
* }
|
|
462
|
+
* // 有副作用的宿主需使用 context.occurrenceId 和持久条件写入。
|
|
463
|
+
* ```
|
|
220
464
|
*/
|
|
221
465
|
interface SchedulerTaskHooks {
|
|
222
466
|
/** 任务开始前触发 */
|
|
@@ -228,14 +472,60 @@ interface SchedulerTaskHooks {
|
|
|
228
472
|
/** 任务结束时触发 */
|
|
229
473
|
onTaskFinish?: (event: SchedulerTaskFinishEvent) => void | Promise<void>;
|
|
230
474
|
}
|
|
231
|
-
/**
|
|
232
|
-
|
|
475
|
+
/**
|
|
476
|
+
* 执行状态
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```ts
|
|
480
|
+
* import type { ExecutionStatus } from '@h-ai/scheduler'
|
|
481
|
+
*
|
|
482
|
+
* function needsReconciliation(status: ExecutionStatus): boolean {
|
|
483
|
+
* return status === 'unknown'
|
|
484
|
+
* }
|
|
485
|
+
* // unknown 先隔离旧执行并核对外部效果,不换键重试。
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
type ExecutionStatus = 'success' | 'failed' | 'interrupted' | 'unknown';
|
|
489
|
+
/**
|
|
490
|
+
* 日志投影状态,与业务 status 独立;unconfirmed 表示没有可补写的已确认终态。
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* import type { TaskLogPersistence } from '@h-ai/scheduler'
|
|
495
|
+
*
|
|
496
|
+
* const persistence: TaskLogPersistence = { status: 'pending', error: { code: 'hai:scheduler:032', message: 'Log projection pending' } }
|
|
497
|
+
* const needsRepair = persistence.status === 'pending'
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
interface TaskLogPersistence {
|
|
501
|
+
/** pending 可由已发布 journal 补写;persisted 已确认日志和回执;unconfirmed 不能推断业务结果。 */
|
|
502
|
+
status: 'pending' | 'persisted' | 'unconfirmed';
|
|
503
|
+
/** 安全的公开错误,不包含 SQL、驱动 cause 或连接参数。 */
|
|
504
|
+
error?: {
|
|
505
|
+
code: string;
|
|
506
|
+
message: string;
|
|
507
|
+
};
|
|
508
|
+
}
|
|
233
509
|
/**
|
|
234
510
|
* 任务执行日志
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* ```ts
|
|
514
|
+
* import type { TaskExecutionLog } from '@h-ai/scheduler'
|
|
515
|
+
*
|
|
516
|
+
* function summarize(log: TaskExecutionLog) {
|
|
517
|
+
* return { businessStatus: log.status, logStatus: log.persistence.status, occurrenceId: log.occurrenceId }
|
|
518
|
+
* }
|
|
519
|
+
* // pending 的成功业务只补写日志,不重跑处理器。
|
|
520
|
+
* ```
|
|
235
521
|
*/
|
|
236
522
|
interface TaskExecutionLog {
|
|
237
|
-
/**
|
|
238
|
-
|
|
523
|
+
/** 持久执行回执;相同定时 occurrence 或手动幂等键共用。 */
|
|
524
|
+
occurrenceId?: string;
|
|
525
|
+
/** 每次尝试固定的 UUID 主键;未知写入回执或并发补写均复用。 */
|
|
526
|
+
id: string;
|
|
527
|
+
/** 独立持久化状态;业务成功不等于日志保存成功。 */
|
|
528
|
+
persistence: TaskLogPersistence;
|
|
239
529
|
/** 任务 ID */
|
|
240
530
|
taskId: string;
|
|
241
531
|
/** 任务名称 */
|
|
@@ -248,7 +538,7 @@ interface TaskExecutionLog {
|
|
|
248
538
|
triggerSource: string | null;
|
|
249
539
|
/** 执行状态 */
|
|
250
540
|
status: ExecutionStatus;
|
|
251
|
-
/**
|
|
541
|
+
/** 序列化 JS/Hook 结果或 API 响应正文;无结果为 null */
|
|
252
542
|
result: string | null;
|
|
253
543
|
/** 错误或中断信息 */
|
|
254
544
|
error: string | null;
|
|
@@ -261,6 +551,17 @@ interface TaskExecutionLog {
|
|
|
261
551
|
}
|
|
262
552
|
/**
|
|
263
553
|
* 执行日志查询选项
|
|
554
|
+
*
|
|
555
|
+
* @example
|
|
556
|
+
* ```ts
|
|
557
|
+
* import type { LogQueryOptions } from '@h-ai/scheduler'
|
|
558
|
+
* import { scheduler } from '@h-ai/scheduler'
|
|
559
|
+
*
|
|
560
|
+
* const options: LogQueryOptions = { taskId: 'daily-report', status: 'success', pagination: { page: 1, pageSize: 20 } }
|
|
561
|
+
* const logs = await scheduler.getLogs(options)
|
|
562
|
+
* if (!logs.success) throw new Error(logs.error.message)
|
|
563
|
+
* const page = logs.data.items
|
|
564
|
+
* ```
|
|
264
565
|
*/
|
|
265
566
|
interface LogQueryOptions {
|
|
266
567
|
/** 按任务 ID 过滤 */
|
|
@@ -278,7 +579,17 @@ interface LogQueryOptions {
|
|
|
278
579
|
/** 分页参数(page 从 1 开始,pageSize 默认 20) */
|
|
279
580
|
pagination?: PaginationOptionsInput;
|
|
280
581
|
}
|
|
281
|
-
/**
|
|
582
|
+
/**
|
|
583
|
+
* 日志清理策略
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```ts
|
|
587
|
+
* import type { SchedulerLogCleanupPolicy } from '@h-ai/scheduler'
|
|
588
|
+
*
|
|
589
|
+
* const policy: SchedulerLogCleanupPolicy = { maxLogs: 1000, retentionDays: 30 }
|
|
590
|
+
* // 仅清理日志投影,不撤销或重排共享 execution journal。
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
282
593
|
interface SchedulerLogCleanupPolicy {
|
|
283
594
|
/** 最多保留日志条数 */
|
|
284
595
|
maxLogs?: number;
|
|
@@ -290,15 +601,28 @@ interface SchedulerLogCleanupPolicy {
|
|
|
290
601
|
*
|
|
291
602
|
* 可更新的字段包括:名称、cron 表达式、启用状态、参数、处理器。
|
|
292
603
|
* 任务 ID 不可更新。
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```ts
|
|
607
|
+
* import type { TaskUpdateInput } from '@h-ai/scheduler'
|
|
608
|
+
* import { scheduler } from '@h-ai/scheduler'
|
|
609
|
+
*
|
|
610
|
+
* const updates: TaskUpdateInput = { enabled: false, timezone: null }
|
|
611
|
+
* const changed = await scheduler.updateTask('daily-report', updates)
|
|
612
|
+
* if (!changed.success) throw new Error(changed.error.message)
|
|
613
|
+
* // null 时区恢复 UTC,任务 ID 不能修改。
|
|
614
|
+
* ```
|
|
293
615
|
*/
|
|
294
616
|
interface TaskUpdateInput {
|
|
617
|
+
/** 全部副作用的持久去重能力声明。 */
|
|
618
|
+
idempotent?: boolean;
|
|
295
619
|
/** 任务名称 */
|
|
296
620
|
name?: string;
|
|
297
621
|
/** 任务描述 */
|
|
298
622
|
description?: string;
|
|
299
623
|
/** cron 表达式 */
|
|
300
624
|
cron?: string;
|
|
301
|
-
/** IANA 时区;传 null
|
|
625
|
+
/** IANA 时区;传 null 表示清空并恢复 UTC,不读取节点 TZ。 */
|
|
302
626
|
timezone?: string | null;
|
|
303
627
|
/** 是否启用 */
|
|
304
628
|
enabled?: boolean;
|
|
@@ -311,15 +635,122 @@ interface TaskUpdateInput {
|
|
|
311
635
|
/** 任务处理器;传 null 表示清空内置处理器 */
|
|
312
636
|
handler?: TaskHandlerConfig | null;
|
|
313
637
|
}
|
|
314
|
-
/**
|
|
638
|
+
/**
|
|
639
|
+
* 手工触发输入
|
|
640
|
+
*
|
|
641
|
+
* @example
|
|
642
|
+
* ```ts
|
|
643
|
+
* import type { TriggerTaskInput } from '@h-ai/scheduler'
|
|
644
|
+
* import { scheduler } from '@h-ai/scheduler'
|
|
645
|
+
*
|
|
646
|
+
* const options: TriggerTaskInput = { source: 'billing-console', idempotencyKey: 'billing-2026-10' }
|
|
647
|
+
* const result = await scheduler.trigger('daily-report', options)
|
|
648
|
+
* if (!result.success) throw new Error(result.error.message)
|
|
649
|
+
* const status = result.data.status
|
|
650
|
+
* ```
|
|
651
|
+
*/
|
|
315
652
|
interface TriggerTaskInput {
|
|
316
653
|
/** 手工触发来源(例如 admin-console / api / cli) */
|
|
317
654
|
source?: string;
|
|
655
|
+
/** 稳定业务触发键(1–128 字符);同任务重复使用会命中同一持久执行。 */
|
|
656
|
+
idempotencyKey?: string;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* 可信运维可见的执行状态;unknown 继续占用任务串行槽。
|
|
660
|
+
*
|
|
661
|
+
* @example
|
|
662
|
+
* ```ts
|
|
663
|
+
* import type { SchedulerExecutionState } from '@h-ai/scheduler'
|
|
664
|
+
*
|
|
665
|
+
* function occupiesUncertainSlot(state: SchedulerExecutionState): boolean {
|
|
666
|
+
* return state === 'unknown'
|
|
667
|
+
* }
|
|
668
|
+
* ```
|
|
669
|
+
*/
|
|
670
|
+
type SchedulerExecutionState = 'claimed' | 'running' | ExecutionStatus | 'discarded';
|
|
671
|
+
/**
|
|
672
|
+
* 不包含代码、参数、URL 或 owner 凭据的共享执行记录。
|
|
673
|
+
*
|
|
674
|
+
* @example
|
|
675
|
+
* ```ts
|
|
676
|
+
* import type { SchedulerExecutionEntry } from '@h-ai/scheduler'
|
|
677
|
+
*
|
|
678
|
+
* function publicAudit(entry: SchedulerExecutionEntry) {
|
|
679
|
+
* return { occurrenceId: entry.occurrenceId, status: entry.status, revision: entry.updatedAt }
|
|
680
|
+
* }
|
|
681
|
+
* // 安全投影不包含 owner token 和业务载荷。
|
|
682
|
+
* ```
|
|
683
|
+
*/
|
|
684
|
+
interface SchedulerExecutionEntry {
|
|
685
|
+
/** 稳定执行回执。 */
|
|
686
|
+
occurrenceId: string;
|
|
687
|
+
/** 任务标识。 */
|
|
688
|
+
taskId: string;
|
|
689
|
+
/** 共享状态。 */
|
|
690
|
+
status: SchedulerExecutionState;
|
|
691
|
+
/** 数据库序号,配合 attempt 用于外部 fencing。 */
|
|
692
|
+
sequence: number;
|
|
693
|
+
/** 已开始的尝试次数。 */
|
|
694
|
+
attemptCount: number;
|
|
695
|
+
/** 最近状态版本(Unix 毫秒),用于人工裁定条件。 */
|
|
696
|
+
updatedAt: number;
|
|
697
|
+
/** 日志投影独立状态;pending 不会重新排队业务。 */
|
|
698
|
+
logPersistence: TaskLogPersistence['status'];
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* 可信服务端的执行查询与人工核对入口。
|
|
702
|
+
*
|
|
703
|
+
* @example
|
|
704
|
+
* ```ts
|
|
705
|
+
* import type { SchedulerExecutionOperations } from '@h-ai/scheduler'
|
|
706
|
+
*
|
|
707
|
+
* async function repairPending(execution: SchedulerExecutionOperations) {
|
|
708
|
+
* const pending = await execution.listLogFailures({ limit: 20 })
|
|
709
|
+
* if (!pending.success) return pending
|
|
710
|
+
* for (const entry of pending.data) {
|
|
711
|
+
* const repaired = await execution.repairLog(entry.occurrenceId)
|
|
712
|
+
* if (!repaired.success) return repaired
|
|
713
|
+
* }
|
|
714
|
+
* }
|
|
715
|
+
* // 此链路不会调用业务 handler。
|
|
716
|
+
* ```
|
|
717
|
+
*/
|
|
718
|
+
interface SchedulerExecutionOperations {
|
|
719
|
+
/** 按回执查询安全元数据。@example await scheduler.execution.get(occurrenceId) */
|
|
720
|
+
get: (occurrenceId: string) => Promise<HaiResult<SchedulerExecutionEntry | null>>;
|
|
721
|
+
/** 有界查询待核对的 unknown;每页 1–100 条。@example await scheduler.execution.listUnknown({ limit: 50, offset: 0 }) */
|
|
722
|
+
listUnknown: (options?: {
|
|
723
|
+
limit?: number;
|
|
724
|
+
offset?: number;
|
|
725
|
+
}) => Promise<HaiResult<SchedulerExecutionEntry[]>>;
|
|
726
|
+
/** 分页查询已有终态但日志尚待补写的回执,不含业务载荷。@example await scheduler.execution.listLogFailures({ limit: 50 }) */
|
|
727
|
+
listLogFailures: (options?: {
|
|
728
|
+
limit?: number;
|
|
729
|
+
offset?: number;
|
|
730
|
+
}) => Promise<HaiResult<SchedulerExecutionEntry[]>>;
|
|
731
|
+
/** 只从已发布 journal 幂等补写日志,不执行业务;并发调用安全。@example await scheduler.execution.repairLog(occurrenceId) */
|
|
732
|
+
repairLog: (occurrenceId: string) => Promise<HaiResult<void>>;
|
|
733
|
+
/** 先终止旧 runner 并核对外部结果,再条件裁定;不能重新排队。@example await scheduler.execution.resolveUnknown(id, { status: 'discarded', expectedUpdatedAt: entry.updatedAt }) */
|
|
734
|
+
resolveUnknown: (occurrenceId: string, resolution: {
|
|
735
|
+
status: 'success' | 'discarded';
|
|
736
|
+
expectedUpdatedAt: number;
|
|
737
|
+
}) => Promise<HaiResult<void>>;
|
|
318
738
|
}
|
|
319
739
|
/**
|
|
320
740
|
* 调度器初始化输入
|
|
321
741
|
*
|
|
322
742
|
* 包含调度器配置、可选的预定义任务列表,以及可选的全局生命周期回调。
|
|
743
|
+
*
|
|
744
|
+
* @example
|
|
745
|
+
* ```ts
|
|
746
|
+
* import type { SchedulerInitInput } from '@h-ai/scheduler'
|
|
747
|
+
* import { scheduler } from '@h-ai/scheduler'
|
|
748
|
+
*
|
|
749
|
+
* // 宿主已成功初始化共享 reldb。
|
|
750
|
+
* const input: SchedulerInitInput = { leaseMs: 60000, tasks: [{ id: 'daily-report', name: '每日报表', cron: '0 2 * * *', timezone: 'UTC' }], hooks: { onTaskExecute: ({ context }) => ({ taskId: context.taskId }) } }
|
|
751
|
+
* const initialized = await scheduler.init(input)
|
|
752
|
+
* if (!initialized.success) throw new Error(initialized.error.message)
|
|
753
|
+
* ```
|
|
323
754
|
*/
|
|
324
755
|
interface SchedulerInitInput extends SchedulerConfigInput {
|
|
325
756
|
/** 预定义任务列表(从配置中加载,初始化时自动注册) */
|
|
@@ -329,8 +760,22 @@ interface SchedulerInitInput extends SchedulerConfigInput {
|
|
|
329
760
|
}
|
|
330
761
|
/**
|
|
331
762
|
* 调度器函数接口
|
|
763
|
+
*
|
|
764
|
+
* @example
|
|
765
|
+
* ```ts
|
|
766
|
+
* import type { SchedulerFunctions } from '@h-ai/scheduler'
|
|
767
|
+
*
|
|
768
|
+
* async function stopScheduler(instance: SchedulerFunctions) {
|
|
769
|
+
* const stopped = instance.stop()
|
|
770
|
+
* if (!stopped.success) return stopped
|
|
771
|
+
* await instance.close()
|
|
772
|
+
* }
|
|
773
|
+
* // close 返回 Promise<void>,宿主随后关闭自己拥有的共享数据库。
|
|
774
|
+
* ```
|
|
332
775
|
*/
|
|
333
776
|
interface SchedulerFunctions {
|
|
777
|
+
/** 共享执行状态、未知结果查询与可信人工裁定。 */
|
|
778
|
+
readonly execution: SchedulerExecutionOperations;
|
|
334
779
|
/** 刷新共享数据库中的任务定义,用于跨实例配置读取。 */
|
|
335
780
|
refreshTasks: () => Promise<HaiResult<void>>;
|
|
336
781
|
/** 初始化调度器 */
|
|
@@ -375,6 +820,38 @@ interface SchedulerFunctions {
|
|
|
375
820
|
* @module scheduler-main
|
|
376
821
|
*/
|
|
377
822
|
|
|
823
|
+
/**
|
|
824
|
+
* 定时任务公共入口;先初始化共享 RelDB,再注册/触发/启动;关闭会排空任务和日志修复,宿主最后关闭数据库。
|
|
825
|
+
*
|
|
826
|
+
* @example
|
|
827
|
+
* ```ts
|
|
828
|
+
* import { core } from '@h-ai/core'
|
|
829
|
+
* import { reldb } from '@h-ai/reldb'
|
|
830
|
+
* import { scheduler } from '@h-ai/scheduler'
|
|
831
|
+
*
|
|
832
|
+
* // 本地开发示例;生产使用所有节点共同连接的 PostgreSQL/MySQL。
|
|
833
|
+
* const database = await reldb.init({ type: 'sqlite', database: ':memory:' })
|
|
834
|
+
* if (!database.success) throw new Error(database.error.message)
|
|
835
|
+
* try {
|
|
836
|
+
* const initialized = await scheduler.init({ hooks: { onTaskExecute: ({ context }) => ({ taskId: context.taskId }) } })
|
|
837
|
+
* if (!initialized.success) throw new Error(initialized.error.message)
|
|
838
|
+
* const registered = await scheduler.register({ id: 'daily-report', name: '每日报表', cron: '0 2 * * *', timezone: 'UTC' })
|
|
839
|
+
* if (!registered.success) throw new Error(registered.error.message)
|
|
840
|
+
* const executed = await scheduler.trigger('daily-report', { idempotencyKey: 'report-2026-10-23' })
|
|
841
|
+
* if (!executed.success) throw new Error(executed.error.message)
|
|
842
|
+
* // 业务 status 与 persistence 分开判断;pending 只补日志,不重跑业务。
|
|
843
|
+
* const status = executed.data.status
|
|
844
|
+
* const persistence = executed.data.persistence.status
|
|
845
|
+
* } finally {
|
|
846
|
+
* await scheduler.close()
|
|
847
|
+
* const closed = await reldb.close()
|
|
848
|
+
* if (!closed.success) {
|
|
849
|
+
* core.logger.error('Database cleanup failed', { errorCode: closed.error.code })
|
|
850
|
+
* process.exitCode = 1
|
|
851
|
+
* }
|
|
852
|
+
* }
|
|
853
|
+
* ```
|
|
854
|
+
*/
|
|
378
855
|
declare const scheduler: SchedulerFunctions;
|
|
379
856
|
|
|
380
|
-
export { type ApiTaskConfig, type ExecutionStatus, HaiSchedulerError, type JsTaskConfig, type JsTaskHandler, type LogQueryOptions, type SchedulerConfig, type SchedulerConfigInput, SchedulerConfigSchema, type SchedulerFunctions, type SchedulerInitInput, type SchedulerLogCleanupPolicy, type SchedulerTaskContext, type SchedulerTaskExecuteEvent, type SchedulerTaskFinishEvent, type SchedulerTaskHooks, type SchedulerTaskInterruptedEvent, type SchedulerTaskStartEvent, type TaskDefinition, type TaskExecutionLog, type TaskExecutionTargetType, type TaskHandlerConfig, type TaskHandlerKind, type TaskParams, type TaskRetryPolicy, type TaskTriggerInfo, type TaskTriggerType, type TaskUpdateInput, type TriggerTaskInput, scheduler };
|
|
857
|
+
export { type ApiTaskConfig, ExecutionResolutionSchema, type ExecutionStatus, HaiSchedulerError, type JsTaskConfig, type JsTaskHandler, type LogQueryOptions, type SchedulerConfig, type SchedulerConfigInput, SchedulerConfigSchema, type SchedulerExecutionEntry, type SchedulerExecutionOperations, type SchedulerExecutionState, type SchedulerFunctions, type SchedulerInitInput, type SchedulerLogCleanupPolicy, type SchedulerTaskContext, type SchedulerTaskExecuteEvent, type SchedulerTaskFinishEvent, type SchedulerTaskHooks, type SchedulerTaskInterruptedEvent, type SchedulerTaskStartEvent, type TaskDefinition, type TaskExecutionLog, type TaskExecutionTargetType, type TaskHandlerConfig, type TaskHandlerKind, type TaskLogPersistence, type TaskParams, type TaskRetryPolicy, type TaskTriggerInfo, type TaskTriggerType, type TaskUpdateInput, type TriggerTaskInput, TriggerTaskSchema, scheduler };
|