befly 3.75.3 → 3.76.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/checks/corns.js +54 -0
- package/index.js +16 -1
- package/package.json +1 -1
- package/paths.js +14 -0
- package/plugins/cron.js +31 -10
- package/utils/is.js +14 -0
- package/utils/scanSources.js +16 -2
package/checks/corns.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Logger } from "../lib/logger.js";
|
|
2
|
+
import { formatValidationIssues } from "../utils/formatValidationIssues.js";
|
|
3
|
+
import { isFunction, isPlainObject, isValidCronSchedule } from "../utils/is.js";
|
|
4
|
+
|
|
5
|
+
// scanFiles 基础字段 + 定时器业务字段(schedule/handler;时区统一走 config.tz)
|
|
6
|
+
const cornKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "apiPath", "schedule", "handler"]);
|
|
7
|
+
|
|
8
|
+
export const checkCorn = (corns) => {
|
|
9
|
+
if (!Array.isArray(corns)) {
|
|
10
|
+
const errors = formatValidationIssues([{ path: [], message: "必须是数组" }], { item: corns, itemLabel: "corn" });
|
|
11
|
+
Logger.warn(`定时器校验失败`, { errors: errors });
|
|
12
|
+
return errors;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const errors = [];
|
|
16
|
+
|
|
17
|
+
// 任务名唯一性:文件名即任务名(core + app 合并后全局唯一,防止同名任务被 register 静默去重)
|
|
18
|
+
const nameMap = new Map();
|
|
19
|
+
for (const corn of corns) {
|
|
20
|
+
if (!corn?.fileName) continue;
|
|
21
|
+
nameMap.set(corn.fileName, (nameMap.get(corn.fileName) || 0) + 1);
|
|
22
|
+
}
|
|
23
|
+
const duplicateNames = [...nameMap].filter(([, count]) => count > 1).map(([name]) => name);
|
|
24
|
+
if (duplicateNames.length > 0) {
|
|
25
|
+
const duplicateCorns = corns.filter((corn) => duplicateNames.includes(corn.fileName));
|
|
26
|
+
errors.push(
|
|
27
|
+
...formatValidationIssues(
|
|
28
|
+
duplicateCorns.map((corn, index) => ({ path: [index], message: `任务名 ${corn.fileName} 重复,请显式区分` })),
|
|
29
|
+
{ items: duplicateCorns, itemLabel: "corn" }
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const [index, corn] of corns.entries()) {
|
|
35
|
+
if (!isPlainObject(corn)) {
|
|
36
|
+
errors.push(...formatValidationIssues([{ path: [index], message: "必须是对象格式" }], { items: corns, itemLabel: "corn" }));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const issues = [];
|
|
41
|
+
for (const key of Object.keys(corn)) {
|
|
42
|
+
if (!cornKeys.has(key)) issues.push({ path: [key], message: "不允许出现" });
|
|
43
|
+
}
|
|
44
|
+
if (!isFunction(corn.handler)) issues.push({ path: ["handler"], message: "必须是函数" });
|
|
45
|
+
if (!isValidCronSchedule(corn.schedule)) issues.push({ path: ["schedule"], message: "必须是可注册的 cron 表达式" });
|
|
46
|
+
if (issues.length === 0) continue;
|
|
47
|
+
errors.push(...formatValidationIssues(issues, { items: corns, itemLabel: "corn" }));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (errors.length > 0) {
|
|
51
|
+
Logger.warn(`定时器校验失败`, { errors: errors });
|
|
52
|
+
}
|
|
53
|
+
return errors;
|
|
54
|
+
};
|
package/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// 检查
|
|
7
7
|
import { checkApi } from "#befly/checks/api.js";
|
|
8
8
|
import { checkConfig } from "#befly/checks/config.js";
|
|
9
|
+
import { checkCorn } from "#befly/checks/corns.js";
|
|
9
10
|
import { checkHook, checkPlugin } from "#befly/checks/items.js";
|
|
10
11
|
import { checkMenu } from "#befly/checks/menu.js";
|
|
11
12
|
import { checkTable } from "#befly/checks/table.js";
|
|
@@ -18,6 +19,8 @@ import { BEFLY_ADMIN_TABLE, BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ROLE_TABLE
|
|
|
18
19
|
import { Connect } from "#befly/lib/connect.js";
|
|
19
20
|
// oxlint-disable-next-line unicorn/prefer-export-from -- Logger 同时被本文件大量使用,不能合并为 re-export
|
|
20
21
|
import { Logger } from "#befly/lib/logger.js";
|
|
22
|
+
// 定时任务
|
|
23
|
+
import { registerCorns } from "#befly/plugins/cron.js";
|
|
21
24
|
// 路由处理
|
|
22
25
|
import { apiHandler } from "#befly/router/api.js";
|
|
23
26
|
import { staticHandler } from "#befly/router/static.js";
|
|
@@ -108,7 +111,7 @@ export async function createBefly(config = {}, menus = []) {
|
|
|
108
111
|
Logger.info(`启动 检查配置 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
109
112
|
|
|
110
113
|
stageStartTime = Bun.nanoseconds();
|
|
111
|
-
const { apis, tables, plugins, hooks } = await scanSources();
|
|
114
|
+
const { apis, tables, plugins, hooks, corns } = await scanSources();
|
|
112
115
|
Logger.info(`启动 扫描源码 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
113
116
|
|
|
114
117
|
stageStartTime = Bun.nanoseconds();
|
|
@@ -127,6 +130,10 @@ export async function createBefly(config = {}, menus = []) {
|
|
|
127
130
|
const hookErrors = await checkHook(hooks);
|
|
128
131
|
Logger.info(`启动 检查钩子 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
129
132
|
|
|
133
|
+
stageStartTime = Bun.nanoseconds();
|
|
134
|
+
const cornErrors = await checkCorn(corns);
|
|
135
|
+
Logger.info(`启动 检查定时器 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
136
|
+
|
|
130
137
|
stageStartTime = Bun.nanoseconds();
|
|
131
138
|
const menuErrors = await checkMenu(mergedMenus);
|
|
132
139
|
Logger.info(`启动 检查菜单 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
@@ -138,6 +145,7 @@ export async function createBefly(config = {}, menus = []) {
|
|
|
138
145
|
{ check: "表结构", errors: tableErrors },
|
|
139
146
|
{ check: "插件", errors: pluginErrors },
|
|
140
147
|
{ check: "钩子", errors: hookErrors },
|
|
148
|
+
{ check: "定时器", errors: cornErrors },
|
|
141
149
|
{ check: "菜单", errors: menuErrors }
|
|
142
150
|
].filter((entry) => entry.errors.length > 0);
|
|
143
151
|
|
|
@@ -161,6 +169,7 @@ export async function createBefly(config = {}, menus = []) {
|
|
|
161
169
|
apis: apis,
|
|
162
170
|
hooks: hooks,
|
|
163
171
|
plugins: plugins,
|
|
172
|
+
corns: corns,
|
|
164
173
|
createdByFactory: true
|
|
165
174
|
});
|
|
166
175
|
}
|
|
@@ -179,6 +188,7 @@ export class Befly {
|
|
|
179
188
|
this.hooks = Array.isArray(init.hooks) ? init.hooks : [];
|
|
180
189
|
this.apis = Array.isArray(init.apis) ? init.apis : [];
|
|
181
190
|
this.plugins = Array.isArray(init.plugins) ? init.plugins : [];
|
|
191
|
+
this.corns = Array.isArray(init.corns) ? init.corns : [];
|
|
182
192
|
this.createdByFactory = init.createdByFactory === true;
|
|
183
193
|
}
|
|
184
194
|
|
|
@@ -217,6 +227,11 @@ export class Befly {
|
|
|
217
227
|
Logger.info(`启动 插件 ${item.fileName} 耗时 ${calcPerfTime(pluginStartTime)}`);
|
|
218
228
|
}
|
|
219
229
|
|
|
230
|
+
// 注册定时器(依赖插件加载完成的 ctx.cron,非主进程自动跳过)
|
|
231
|
+
stageStartTime = Bun.nanoseconds();
|
|
232
|
+
registerCorns(this.context, this.corns);
|
|
233
|
+
Logger.info(`启动 注册定时器 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
234
|
+
|
|
220
235
|
stageStartTime = Bun.nanoseconds();
|
|
221
236
|
await syncReady(this.context);
|
|
222
237
|
Logger.info(`启动 同步就绪 耗时 ${calcPerfTime(stageStartTime)}`);
|
package/package.json
CHANGED
package/paths.js
CHANGED
|
@@ -46,6 +46,13 @@ export const coreCheckDir = join(moduleDir, "checks");
|
|
|
46
46
|
*/
|
|
47
47
|
export const corePluginDir = join(moduleDir, "plugins");
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Core 框架定时器目录
|
|
51
|
+
* @description packages/core/corns/
|
|
52
|
+
* @usage 存放框架内置定时任务(每文件一个定时器)
|
|
53
|
+
*/
|
|
54
|
+
export const coreCornDir = join(moduleDir, "corns");
|
|
55
|
+
|
|
49
56
|
/**
|
|
50
57
|
* Core 框架钩子目录
|
|
51
58
|
* @description packages/core/hooks/
|
|
@@ -90,6 +97,13 @@ export const appCheckDir = join(appDir, "checks");
|
|
|
90
97
|
*/
|
|
91
98
|
export const appPluginDir = join(appDir, "plugins");
|
|
92
99
|
|
|
100
|
+
/**
|
|
101
|
+
* 项目定时器目录
|
|
102
|
+
* @description {appDir}/corns/
|
|
103
|
+
* @usage 存放用户业务定时任务(每文件一个定时器)
|
|
104
|
+
*/
|
|
105
|
+
export const appCornDir = join(appDir, "corns");
|
|
106
|
+
|
|
93
107
|
/**
|
|
94
108
|
* 项目钩子目录
|
|
95
109
|
* @description {appDir}/hooks/
|
package/plugins/cron.js
CHANGED
|
@@ -4,7 +4,8 @@ import { PLUGIN_ORDER } from "../configs/constConfig.js";
|
|
|
4
4
|
* 基于 Bun.cron 提供进程内定时任务能力。
|
|
5
5
|
*/
|
|
6
6
|
import { Logger } from "../lib/logger.js";
|
|
7
|
-
import {
|
|
7
|
+
import { createError } from "../utils/error.js";
|
|
8
|
+
import { isPrimaryProcess, isValidCronSchedule } from "../utils/is.js";
|
|
8
9
|
|
|
9
10
|
const jobs = new Map();
|
|
10
11
|
|
|
@@ -24,15 +25,26 @@ function register(context, name, schedule, task) {
|
|
|
24
25
|
return jobs.get(name);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
// 前置校验:表达式非法时启动即报错,避免 Bun.cron 注册时抛裸错
|
|
29
|
+
if (!isValidCronSchedule(schedule)) {
|
|
30
|
+
throw createError(`定时任务 ${name} 的 schedule 不是合法的 cron 表达式`, { code: "validation", subsystem: "cron", operation: "register" });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 时区统一走 config.tz(checkConfig 已校验 IANA 合法性),未配置时使用系统时区
|
|
34
|
+
const tz = context.config?.tz;
|
|
35
|
+
const job = Bun.cron(
|
|
36
|
+
schedule,
|
|
37
|
+
async function () {
|
|
38
|
+
Logger.info(`[定时任务] ${name} 开始执行`);
|
|
39
|
+
try {
|
|
40
|
+
await task(context);
|
|
41
|
+
Logger.info(`[定时任务] ${name} 执行完成`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
Logger.error(`[定时任务] ${name} 执行失败`, error);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
tz ? { tz: tz } : undefined
|
|
47
|
+
);
|
|
36
48
|
|
|
37
49
|
jobs.set(name, job);
|
|
38
50
|
return job;
|
|
@@ -73,6 +85,15 @@ function second(context, name, seconds, task) {
|
|
|
73
85
|
return job;
|
|
74
86
|
}
|
|
75
87
|
|
|
88
|
+
/**
|
|
89
|
+
* 批量注册扫描到的定时器(corns/*.js),任务名取文件名
|
|
90
|
+
*/
|
|
91
|
+
export function registerCorns(context, corns) {
|
|
92
|
+
for (const corn of corns) {
|
|
93
|
+
register(context, corn.fileName, corn.schedule, corn.handler);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
/**
|
|
77
98
|
* 停止所有已注册的定时任务
|
|
78
99
|
*/
|
package/utils/is.js
CHANGED
|
@@ -187,6 +187,20 @@ export function isValidTimeZone(value) {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* 判断值是否为可注册的 cron 表达式(与 Bun.cron 注册行为一致:
|
|
192
|
+
* parse 抛异常或 8 年内无匹配都视为非法,避免启动后才报错)。
|
|
193
|
+
*/
|
|
194
|
+
export function isValidCronSchedule(value) {
|
|
195
|
+
if (!isString(value)) return false;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
return Bun.cron.parse(value) !== null;
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
190
204
|
/**
|
|
191
205
|
* 判断值是否为有限整数。
|
|
192
206
|
*/
|
package/utils/scanSources.js
CHANGED
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
coreHookDir,
|
|
8
8
|
appHookDir,
|
|
9
9
|
coreApiDir,
|
|
10
|
-
appApiDir
|
|
10
|
+
appApiDir,
|
|
11
|
+
coreCornDir,
|
|
12
|
+
appCornDir
|
|
11
13
|
} from "../paths.js";
|
|
12
14
|
import { createError } from "./error.js";
|
|
13
15
|
import { scanFiles } from "./scanFiles.js";
|
|
@@ -17,6 +19,7 @@ export const scanSources = async () => {
|
|
|
17
19
|
const plugins = [];
|
|
18
20
|
const hooks = [];
|
|
19
21
|
const tables = {};
|
|
22
|
+
const corns = [];
|
|
20
23
|
|
|
21
24
|
// 处理表格
|
|
22
25
|
const allCoreTables = await scanFiles(coreTableDir, "core", "table", "*.json");
|
|
@@ -48,6 +51,16 @@ export const scanSources = async () => {
|
|
|
48
51
|
hooks.push(item);
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
// 处理定时器
|
|
55
|
+
const allCoreCorns = await scanFiles(coreCornDir, "core", "corn", "*.js");
|
|
56
|
+
for (const item of allCoreCorns) {
|
|
57
|
+
corns.push(item);
|
|
58
|
+
}
|
|
59
|
+
const allAppCorns = await scanFiles(appCornDir, "app", "corn", "*.js");
|
|
60
|
+
for (const item of allAppCorns) {
|
|
61
|
+
corns.push(item);
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
// 处理接口
|
|
52
65
|
const allCoreApis = await scanFiles(coreApiDir, "core", "api", "**/*.js");
|
|
53
66
|
for (const item of allCoreApis) {
|
|
@@ -70,6 +83,7 @@ export const scanSources = async () => {
|
|
|
70
83
|
hooks: hooks,
|
|
71
84
|
plugins: plugins,
|
|
72
85
|
apis: apis,
|
|
73
|
-
tables: tables
|
|
86
|
+
tables: tables,
|
|
87
|
+
corns: corns
|
|
74
88
|
};
|
|
75
89
|
};
|