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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.75.3",
3
+ "version": "3.76.0",
4
4
  "gitHead": "49c39d36695036e85fc64083cc43c1652fff96cb",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
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 { isPrimaryProcess } from "../utils/is.js";
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
- const job = Bun.cron(schedule, async function () {
28
- Logger.info(`[定时任务] ${name} 开始执行`);
29
- try {
30
- await task(context);
31
- Logger.info(`[定时任务] ${name} 执行完成`);
32
- } catch (error) {
33
- Logger.error(`[定时任务] ${name} 执行失败`, error);
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
  */
@@ -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
  };