@hile/schedule 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @hile/schedule
2
+
3
+ Declarative job scheduler based on [node-schedule](https://github.com/node-schedule/node-schedule).
4
+
5
+ ## Usage
6
+
7
+ ### Code-defined jobs
8
+
9
+ ```ts
10
+ import { Scheduler, defineJob } from '@hile/schedule'
11
+ import { second } from '@hile/schedule'
12
+
13
+ const scheduler = new Scheduler()
14
+
15
+ // Cron expression
16
+ scheduler.add('daily-report', '0 8 * * *', () => {
17
+ console.log('generating daily report...')
18
+ })
19
+
20
+ // Delay (毫秒)
21
+ scheduler.add('delayed-task', { delay: 5000 }, () => {
22
+ console.log('ran after 5 seconds')
23
+ })
24
+
25
+ scheduler.stop() // cancel all jobs
26
+ ```
27
+
28
+ ### Auto-load from directory
29
+
30
+ Create files with `{name}.schedule.ts`:
31
+
32
+ ```ts
33
+ // tasks/daily-report.schedule.ts
34
+ import { defineJob } from '@hile/schedule'
35
+
36
+ export default defineJob('0 8 * * *', () => {
37
+ console.log('daily report generated')
38
+ })
39
+ ```
40
+
41
+ Load them:
42
+
43
+ ```ts
44
+ const scheduler = new Scheduler()
45
+ const off = await scheduler.load(join(__dirname, 'tasks'))
46
+ // off() to unregister all loaded jobs
47
+ ```
48
+
49
+ Custom suffix via `suffix` option:
50
+
51
+ ```ts
52
+ await scheduler.load('./jobs', { suffix: 'job' })
53
+ // loads *.job.ts, *.job.js, etc.
54
+ ```
55
+
56
+ ### API
57
+
58
+ #### `defineJob(expression, handler)`
59
+
60
+ - `expression: string | { delay: number }` — cron 表达式或延迟毫秒数
61
+ - Returns `{ id: number, type: 'job', expression, handler }`
62
+
63
+ #### `scheduler.add(id, expression | { delay }, handler)`
64
+
65
+ - `id: string | number` — 任务唯一标识,重复添加抛异常
66
+
67
+ #### `scheduler.remove(id)`
68
+
69
+ #### `scheduler.stop()`
70
+
71
+ 取消所有任务
72
+
73
+ #### `scheduler.getJobs(): JobInfo[]`
74
+
75
+ 返回已注册任务列表
76
+
77
+ #### `scheduler.load(directory, options?)`
78
+
79
+ 自动发现并注册任务文件,返回注销函数
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,8 @@
1
+ import { Scheduler } from './scheduler.js';
2
+ import type { JobDefinition, JobHandler } from './types.js';
3
+ export { Scheduler };
4
+ export type { JobInfo } from './scheduler.js';
5
+ export type { JobDefinition, JobHandler };
6
+ export declare function defineJob(expression: string | {
7
+ delay: number;
8
+ }, handler: () => Promise<void> | void): JobDefinition;
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { Scheduler } from './scheduler.js';
2
+ export { Scheduler };
3
+ let _jobId = 1;
4
+ export function defineJob(expression, handler) {
5
+ return { id: _jobId++, type: 'job', expression, handler };
6
+ }
@@ -0,0 +1,27 @@
1
+ import type { JobHandler } from './types.js';
2
+ export type JobInfo = {
3
+ id: string;
4
+ type: 'cron' | 'delay';
5
+ expression: string;
6
+ };
7
+ export declare class Scheduler {
8
+ private jobs;
9
+ private meta;
10
+ add(id: string | number, expression: string, handler: JobHandler): void;
11
+ add(id: string | number, options: {
12
+ delay: number;
13
+ }, handler: JobHandler): void;
14
+ remove(id: string | number): void;
15
+ stop(): void;
16
+ getJobs(): JobInfo[];
17
+ /**
18
+ * 从目录自动加载 schedule 任务文件
19
+ * 文件后缀: {suffix}.ts/.js,default export 须为 defineJob 返回值
20
+ * @param directory 目录路径
21
+ * @param options.suffix 文件后缀标记,默认 'schedule'
22
+ * @returns 注销函数
23
+ */
24
+ load(directory: string, options?: {
25
+ suffix?: string;
26
+ }): Promise<() => void>;
27
+ }
@@ -0,0 +1,88 @@
1
+ import { scheduleJob } from 'node-schedule';
2
+ import { glob } from 'glob';
3
+ import { resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ export class Scheduler {
6
+ jobs = new Map();
7
+ meta = new Map();
8
+ add(id, exprOrOpts, handler) {
9
+ const key = String(id);
10
+ if (this.jobs.has(key))
11
+ throw new Error(`Job "${key}" already exists`);
12
+ // 包装 handler,捕获异步错误防止 node-schedule 未捕获 rejection
13
+ const safeHandler = () => {
14
+ try {
15
+ const result = handler();
16
+ if (result && typeof result.catch === 'function') {
17
+ result.catch(() => { });
18
+ }
19
+ }
20
+ catch {
21
+ // handler 同步错误也不应影响调度器
22
+ }
23
+ };
24
+ if (typeof exprOrOpts === 'string') {
25
+ this.jobs.set(key, scheduleJob(exprOrOpts, safeHandler));
26
+ this.meta.set(key, { type: 'cron', expression: exprOrOpts });
27
+ }
28
+ else {
29
+ const date = new Date(Date.now() + exprOrOpts.delay);
30
+ this.jobs.set(key, scheduleJob(date, safeHandler));
31
+ this.meta.set(key, { type: 'delay', expression: `delay:${exprOrOpts.delay}` });
32
+ }
33
+ }
34
+ remove(id) {
35
+ const key = String(id);
36
+ const job = this.jobs.get(key);
37
+ if (job) {
38
+ job.cancel();
39
+ this.jobs.delete(key);
40
+ this.meta.delete(key);
41
+ }
42
+ }
43
+ stop() {
44
+ for (const job of this.jobs.values()) {
45
+ job.cancel();
46
+ }
47
+ this.jobs.clear();
48
+ this.meta.clear();
49
+ }
50
+ getJobs() {
51
+ return Array.from(this.meta.entries()).map(([id, m]) => ({
52
+ id,
53
+ type: m.type,
54
+ expression: m.expression,
55
+ }));
56
+ }
57
+ /**
58
+ * 从目录自动加载 schedule 任务文件
59
+ * 文件后缀: {suffix}.ts/.js,default export 须为 defineJob 返回值
60
+ * @param directory 目录路径
61
+ * @param options.suffix 文件后缀标记,默认 'schedule'
62
+ * @returns 注销函数
63
+ */
64
+ async load(directory, options) {
65
+ const suffix = options?.suffix || 'schedule';
66
+ const files = await glob(`**/*.${suffix}.{ts,js,tsx,jsx,mjs}`, { cwd: directory });
67
+ const offFns = [];
68
+ for (const file of files) {
69
+ const filePath = resolve(directory, file);
70
+ const mod = await import(pathToFileURL(filePath).href);
71
+ const jobDef = mod.default;
72
+ if (!jobDef || jobDef.type !== 'job')
73
+ continue;
74
+ const key = String(jobDef.id);
75
+ if (typeof jobDef.expression === 'string') {
76
+ this.add(key, jobDef.expression, jobDef.handler);
77
+ }
78
+ else {
79
+ this.add(key, jobDef.expression, jobDef.handler);
80
+ }
81
+ offFns.push(() => this.remove(key));
82
+ }
83
+ return () => {
84
+ for (const off of offFns)
85
+ off();
86
+ };
87
+ }
88
+ }
@@ -0,0 +1,9 @@
1
+ export type JobHandler = () => Promise<void> | void;
2
+ export type JobDefinition = {
3
+ id: number;
4
+ type: 'job';
5
+ expression: string | {
6
+ delay: number;
7
+ };
8
+ handler: JobHandler;
9
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@hile/schedule",
3
+ "version": "2.0.0",
4
+ "description": "Declarative job scheduler based on node-schedule",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc -b && fix-esm-import-path --preserve-import-type ./dist",
9
+ "dev": "tsc -b --watch",
10
+ "test": "vitest run"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "license": "MIT",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node-schedule": "^2.1.7",
22
+ "fix-esm-import-path": "^1.10.3",
23
+ "vitest": "^4.0.18"
24
+ },
25
+ "dependencies": {
26
+ "glob": "^13.0.6",
27
+ "node-schedule": "^2.1.1"
28
+ },
29
+ "gitHead": "8e0fd1f78b5a8abd21218d1f596ada2533a0c8e7"
30
+ }