@thinkbun/plugin 1.0.3 → 1.0.5

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 CHANGED
@@ -17,6 +17,7 @@ Plugin 包依赖于 ThinkBun Core 包,为其提供扩展功能。这些插件
17
17
  增强应用程序的日志功能,支持彩色输出、作用域标识和不同日志级别。
18
18
 
19
19
  #### 功能特性
20
+
20
21
  - 彩色日志输出,提高可读性
21
22
  - 支持日志作用域标识
22
23
  - 自定义日志级别
@@ -68,6 +69,7 @@ app.logger.warn('警告信息');
68
69
  - `app.logger.warn(message, scope)`: 输出警告级别的日志
69
70
 
70
71
  **参数**:
72
+
71
73
  - `message`: 日志消息内容
72
74
  - `scope`: 可选,日志作用域,可以是字符串或字符串数组
73
75
 
@@ -76,6 +78,7 @@ app.logger.warn('警告信息');
76
78
  提供服务器启动和集群支持功能,优化应用程序的性能和可靠性。
77
79
 
78
80
  #### 功能特性
81
+
79
82
  - 基于 Bun.serve 的高性能服务器
80
83
  - 自动集群管理,充分利用多核 CPU
81
84
  - 工作进程自动重启
@@ -134,6 +137,7 @@ export default {
134
137
  #### 集群模式
135
138
 
136
139
  ServerPlugin 自动支持集群模式:
140
+
137
141
  - 主进程管理工作进程的创建和重启
138
142
  - 工作进程处理实际的 HTTP 请求
139
143
  - 当工作进程意外退出时,自动创建新的工作进程
@@ -143,19 +147,19 @@ ServerPlugin 自动支持集群模式:
143
147
 
144
148
  ### LoggerPlugin 生命周期
145
149
 
146
- | 阶段 | 执行顺序 | 功能 |
147
- |------|----------|------|
148
- | `setup` | `pre` | 初始化日志功能,替换默认日志器 |
149
- | `run` | - | 无 |
150
- | `close` | - | 无 |
150
+ | 阶段 | 执行顺序 | 功能 |
151
+ | ------- | -------- | ------------------------------ |
152
+ | `setup` | `pre` | 初始化日志功能,替换默认日志器 |
153
+ | `run` | - | 无 |
154
+ | `close` | - | 无 |
151
155
 
152
156
  ### ServerPlugin 生命周期
153
157
 
154
- | 阶段 | 执行顺序 | 功能 |
155
- |------|----------|------|
156
- | `setup` | - | 无 |
157
- | `run` | `post` | 启动服务器,根据配置创建工作进程 |
158
- | `close` | - | 优雅关闭服务器和工作进程 |
158
+ | 阶段 | 执行顺序 | 功能 |
159
+ | ------- | -------- | -------------------------------- |
160
+ | `setup` | - | 无 |
161
+ | `run` | `post` | 启动服务器,根据配置创建工作进程 |
162
+ | `close` | - | 优雅关闭服务器和工作进程 |
159
163
 
160
164
  ## 独立运行/测试
161
165
 
@@ -207,6 +211,7 @@ export const CustomPlugin: Plugin = {
207
211
  ## 插件执行顺序
208
212
 
209
213
  插件的执行顺序由 `enforce` 属性决定:
214
+
210
215
  - `pre`: 在核心功能之前执行
211
216
  - `post`: 在核心功能之后执行
212
217
  - 省略: 默认执行顺序
@@ -302,4 +307,4 @@ export default {
302
307
 
303
308
  ---
304
309
 
305
- **ThinkBun Plugin** - 为 ThinkBun 框架提供强大的扩展功能 🧩
310
+ **ThinkBun Plugin** - 为 ThinkBun 框架提供强大的扩展功能 🧩
package/dist/index.d.mts CHANGED
@@ -1,9 +1,18 @@
1
1
  import { Plugin } from "@thinkbun/core";
2
2
 
3
3
  //#region src/logger.d.ts
4
+
5
+ /**
6
+ * 日志插件,为应用添加统一的日志功能
7
+ * 支持不同作用域的彩色日志输出
8
+ */
4
9
  declare const LoggerPlugin: Plugin;
5
10
  //#endregion
6
11
  //#region src/server.d.ts
12
+ /**
13
+ * 服务器插件,提供集群管理和 HTTP 服务功能
14
+ * 支持多进程模式和工作进程自动重启
15
+ */
7
16
  declare const ServerPlugin: Plugin;
8
17
  //#endregion
9
18
  export { LoggerPlugin, ServerPlugin };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/logger.ts","../src/server.ts"],"sourcesContent":[],"mappings":";;;cAOa,cAAc;;;cCEd,cAAc"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/logger.ts","../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;AAoDA;;cAAa,cAAc;;;;;AAA3B;;cC4Ha,cAAc"}
package/dist/index.mjs CHANGED
@@ -1,86 +1,229 @@
1
- import { invariant } from "@thinkbun/core";
1
+ import { LOG_COLORS, invariant } from "@thinkbun/core";
2
2
  import pc from "picocolors";
3
3
  import cluster from "node:cluster";
4
4
  import os from "node:os";
5
5
 
6
6
  //#region src/logger.ts
7
- const colorMap = {
8
- app: "yellow",
9
- PluginManager: "yellow"
7
+ /**
8
+ * 生成作用域前缀
9
+ * @param scope - 日志作用域
10
+ * @returns 格式化的作用域前缀
11
+ */
12
+ const generateScopePrefix = (scope) => {
13
+ if (Array.isArray(scope)) return scope.map((item) => `[${item}]`).join("");
14
+ return `[${scope}]`;
10
15
  };
16
+ /**
17
+ * 获取日志颜色函数
18
+ * @param scope - 日志作用域
19
+ * @returns 颜色函数
20
+ */
21
+ const getLogColor = (scope) => {
22
+ return pc[LOG_COLORS[scope] || "yellow"];
23
+ };
24
+ /**
25
+ * 创建统一的日志方法
26
+ * @param level - 日志级别
27
+ * @returns 日志方法函数
28
+ */
29
+ const createLogMethod = (level) => {
30
+ return (msg, scope = "app") => {
31
+ const consoleMethod = level === "info" ? console.log : level === "error" ? console.error : level === "debug" ? console.debug : console.warn;
32
+ const scopePrefix = generateScopePrefix(scope);
33
+ consoleMethod(getLogColor(scope)(scopePrefix), msg);
34
+ };
35
+ };
36
+ /**
37
+ * 日志插件,为应用添加统一的日志功能
38
+ * 支持不同作用域的彩色日志输出
39
+ */
11
40
  const LoggerPlugin = {
12
41
  name: "LoggerPlugin",
13
42
  enforce: "pre",
14
43
  setup(app) {
15
- const genScope = (scope) => Array.isArray(scope) ? scope.map((el) => `[${el}]`) : `[${scope}]`;
16
44
  app.logger = {
17
- info: (msg, scope = "app") => console.log(pc[colorMap[scope] || "yellow"](genScope(scope)), msg),
18
- error: (msg, scope = "app") => console.error(pc[colorMap[scope] || "yellow"](genScope(scope)), msg),
19
- debug: (msg, scope = "app") => console.debug(pc[colorMap[scope] || "yellow"](genScope(scope)), msg),
20
- warn: (msg, scope = "app") => console.warn(pc[colorMap[scope] || "yellow"](genScope(scope)), msg)
45
+ info: createLogMethod("info"),
46
+ error: createLogMethod("error"),
47
+ debug: createLogMethod("debug"),
48
+ warn: createLogMethod("warn")
21
49
  };
22
50
  }
23
51
  };
24
52
 
25
53
  //#endregion
26
54
  //#region src/server.ts
27
- const isTest = typeof Bun !== "undefined" && Bun.env.NODE_ENV === "test";
28
- let shuttingDown = false;
29
- let isPrimary = false;
55
+ /**
56
+ * 集群关闭超时时间(毫秒)
57
+ */
58
+ const CLUSTER_SHUTDOWN_TIMEOUT = 5e3;
59
+ /**
60
+ * 检查是否为测试环境
61
+ */
62
+ const isTestEnvironment = () => {
63
+ return typeof Bun !== "undefined" && Bun.env.NODE_ENV === "test";
64
+ };
65
+ /**
66
+ * 计算合适的工作进程数量
67
+ * @param configWorkers - 配置中的工作进程数量
68
+ * @returns 实际使用的工作进程数量
69
+ */
70
+ const calculateWorkerCount = (configWorkers) => {
71
+ const cpuCount = os.cpus().length;
72
+ return configWorkers === 0 ? cpuCount : Math.max(1, Math.min(cpuCount, configWorkers));
73
+ };
74
+ /**
75
+ * 启动工作进程
76
+ * @param count - 工作进程数量
77
+ */
78
+ const startWorkers = (count) => {
79
+ for (let i = 0; i < count; i++) try {
80
+ cluster.fork();
81
+ } catch (error) {
82
+ console.error(`Failed to fork worker ${i}:`, error);
83
+ }
84
+ };
85
+ /**
86
+ * 设置工作进程重启逻辑
87
+ * @param shuttingDownRef - 关闭状态引用
88
+ */
89
+ const setupWorkerRestart = (shuttingDownRef) => {
90
+ cluster.on("exit", (worker, code, signal) => {
91
+ if (shuttingDownRef.value) return;
92
+ console.log(`Worker ${worker.process.pid} died (${code}, ${signal}). Restarting...`);
93
+ try {
94
+ cluster.fork();
95
+ } catch (error) {
96
+ console.error("Failed to restart worker:", error);
97
+ }
98
+ });
99
+ };
100
+ /**
101
+ * 启动 HTTP 服务器
102
+ * @param app - 应用实例
103
+ */
104
+ const startHttpServer = (app) => {
105
+ try {
106
+ app.server = Bun.serve({
107
+ port: app.loader.config.port,
108
+ hostname: app.loader.config.host,
109
+ reusePort: true,
110
+ routes: app.routes
111
+ });
112
+ } catch (error) {
113
+ console.error("Failed to start server:", error);
114
+ process.exit(1);
115
+ }
116
+ };
117
+ /**
118
+ * 设置工作进程的消息处理
119
+ * @param app - 应用实例
120
+ */
121
+ const setupWorkerMessageHandler = (app) => {
122
+ process.on("message", async (msg) => {
123
+ if (typeof msg === "object" && msg !== null && msg.type === "shutdown") try {
124
+ await app.server?.stop?.();
125
+ process.exit(0);
126
+ } catch (error) {
127
+ console.error("Error during worker shutdown:", error);
128
+ process.exit(1);
129
+ }
130
+ });
131
+ };
132
+ /**
133
+ * 优雅关闭工作进程
134
+ */
135
+ const gracefulShutdownWorkers = () => {
136
+ return new Promise((resolve) => {
137
+ const workers = cluster.workers || {};
138
+ const workerCount = Object.keys(workers).length;
139
+ if (workerCount === 0) {
140
+ resolve();
141
+ return;
142
+ }
143
+ let exitedWorkers = 0;
144
+ const forceKillTimer = setTimeout(() => {
145
+ console.log("Force killing remaining workers...");
146
+ for (const id in workers) try {
147
+ workers[id]?.kill("SIGKILL");
148
+ } catch (error) {
149
+ console.error(`Failed to kill worker ${id}:`, error);
150
+ }
151
+ resolve();
152
+ }, CLUSTER_SHUTDOWN_TIMEOUT);
153
+ const onWorkerExit = () => {
154
+ exitedWorkers++;
155
+ if (exitedWorkers >= workerCount) {
156
+ clearTimeout(forceKillTimer);
157
+ resolve();
158
+ }
159
+ };
160
+ cluster.on("exit", onWorkerExit);
161
+ for (const id in workers) try {
162
+ workers[id]?.send({ type: "shutdown" });
163
+ } catch (error) {
164
+ console.error(`Failed to send shutdown message to worker ${id}:`, error);
165
+ exitedWorkers++;
166
+ }
167
+ if (exitedWorkers >= workerCount) {
168
+ clearTimeout(forceKillTimer);
169
+ cluster.removeListener("exit", onWorkerExit);
170
+ resolve();
171
+ }
172
+ });
173
+ };
174
+ /**
175
+ * 服务器插件,提供集群管理和 HTTP 服务功能
176
+ * 支持多进程模式和工作进程自动重启
177
+ */
30
178
  const ServerPlugin = {
31
179
  name: "ServerPlugin",
32
180
  enforce: "post",
33
181
  run(app) {
34
- isPrimary = cluster.isPrimary;
35
- const { workers: configWorkers } = app.loader.config;
36
- invariant(typeof configWorkers === "number", "config.workers must be a number");
37
- const cpuCount = os.cpus().length;
38
- const workers = configWorkers === 0 ? cpuCount : Math.max(1, Math.min(cpuCount, configWorkers));
39
- if (isPrimary && workers > 1) {
40
- for (let i = 0; i < workers; i++) cluster.fork();
41
- cluster.on("exit", () => {
42
- if (shuttingDown) return;
43
- cluster.fork();
44
- });
182
+ const isPrimaryProcess = cluster.isPrimary;
183
+ const config = app.loader.config;
184
+ invariant(typeof config.workers === "number", "config.workers must be a number");
185
+ const workerCount = calculateWorkerCount(config.workers);
186
+ const shuttingDownRef = { value: false };
187
+ if (isPrimaryProcess && workerCount > 1) {
188
+ console.log(`Starting ${workerCount} workers...`);
189
+ startWorkers(workerCount);
190
+ setupWorkerRestart(shuttingDownRef);
45
191
  } else {
46
- app.server = Bun.serve({
47
- port: app.loader.config.port,
48
- hostname: app.loader.config.host,
49
- reusePort: true,
50
- routes: app.routes
51
- });
52
- process.on("message", async (msg) => {
53
- if (typeof msg === "object" && msg !== null && msg.type === "shutdown") {
54
- await app.server?.stop?.();
55
- process.exit(0);
56
- }
57
- });
192
+ startHttpServer(app);
193
+ setupWorkerMessageHandler(app);
58
194
  }
195
+ this.isPrimary = isPrimaryProcess;
196
+ this.shuttingDownRef = shuttingDownRef;
59
197
  },
60
198
  async close(app) {
199
+ const isTest = isTestEnvironment();
200
+ const isPrimary = this.isPrimary;
201
+ const shuttingDownRef = this.shuttingDownRef || { value: false };
61
202
  if (isTest) {
62
- await app.server?.stop?.();
203
+ try {
204
+ await app.server?.stop?.();
205
+ } catch (error) {
206
+ console.error("Error stopping server in test mode:", error);
207
+ }
63
208
  return;
64
209
  }
65
210
  if (!isPrimary) {
66
- await app.server?.stop?.();
211
+ try {
212
+ await app.server?.stop?.();
213
+ } catch (error) {
214
+ console.error("Error stopping worker server:", error);
215
+ }
67
216
  process.exit(0);
68
217
  }
69
- if (shuttingDown) return;
70
- shuttingDown = true;
71
- for (const id in cluster.workers) cluster.workers[id]?.send({ type: "shutdown" });
72
- await new Promise((resolve) => {
73
- const timer = setTimeout(() => {
74
- for (const id in cluster.workers) cluster.workers[id]?.kill("SIGKILL");
75
- resolve();
76
- }, 5e3);
77
- cluster.on("exit", () => {
78
- if (Object.keys(cluster?.workers || {}).length === 0) {
79
- clearTimeout(timer);
80
- resolve();
81
- }
82
- });
83
- });
218
+ if (shuttingDownRef.value) return;
219
+ shuttingDownRef.value = true;
220
+ console.log("Shutting down server cluster...");
221
+ try {
222
+ await gracefulShutdownWorkers();
223
+ console.log("All workers shut down successfully");
224
+ } catch (error) {
225
+ console.error("Error during cluster shutdown:", error);
226
+ }
84
227
  process.exit(0);
85
228
  }
86
229
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["LoggerPlugin: Plugin","ServerPlugin: Plugin"],"sources":["../src/logger.ts","../src/server.ts"],"sourcesContent":["import { type Plugin } from '@thinkbun/core';\nimport pc from 'picocolors';\n\nconst colorMap = {\n app: 'yellow',\n PluginManager: 'yellow',\n};\nexport const LoggerPlugin: Plugin = {\n name: 'LoggerPlugin',\n enforce: 'pre',\n setup(app) {\n const genScope = (scope: string | string[]) => (Array.isArray(scope) ? scope.map((el) => `[${el}]`) : `[${scope}]`);\n app.logger = {\n // @ts-ignore\n info: (msg: string, scope = 'app') => console.log(pc[colorMap[scope] || 'yellow'](genScope(scope)), msg),\n // @ts-ignore\n error: (msg: string, scope = 'app') => console.error(pc[colorMap[scope] || 'yellow'](genScope(scope)), msg),\n // @ts-ignore\n debug: (msg: string, scope = 'app') => console.debug(pc[colorMap[scope] || 'yellow'](genScope(scope)), msg),\n // @ts-ignore\n warn: (msg: string, scope = 'app') => console.warn(pc[colorMap[scope] || 'yellow'](genScope(scope)), msg),\n };\n },\n};\n","import cluster from 'node:cluster';\nimport os from 'node:os';\n\nimport { invariant, type Plugin } from '@thinkbun/core';\n\nconst isTest = typeof Bun !== 'undefined' && Bun.env.NODE_ENV === 'test';\n\nlet shuttingDown = false;\nlet isPrimary = false;\nexport const ServerPlugin: Plugin = {\n name: 'ServerPlugin',\n enforce: 'post',\n run(app) {\n isPrimary = cluster.isPrimary;\n const { workers: configWorkers } = app.loader.config;\n invariant(typeof configWorkers === 'number', 'config.workers must be a number');\n const cpuCount = os.cpus().length;\n const workers = configWorkers === 0 ? cpuCount : Math.max(1, Math.min(cpuCount, configWorkers));\n if (isPrimary && workers > 1) {\n for (let i = 0; i < workers; i++) {\n cluster.fork();\n }\n\n (cluster as any).on('exit', () => {\n if (shuttingDown) return;\n cluster.fork();\n });\n } else {\n app.server = Bun.serve({\n port: app.loader.config.port,\n hostname: app.loader.config.host,\n reusePort: true,\n routes: app.routes,\n });\n\n process.on('message', async (msg: { type: string }) => {\n if (typeof msg === 'object' && msg !== null && msg.type === 'shutdown') {\n await app.server?.stop?.();\n process.exit(0);\n }\n });\n }\n },\n async close(app) {\n if (isTest) {\n // 只关闭 server,不退出进程\n await app.server?.stop?.();\n return;\n }\n // 只允许 primary 主动关闭 cluster\n if (!isPrimary) {\n await app.server?.stop?.();\n process.exit(0);\n }\n\n if (shuttingDown) return;\n shuttingDown = true;\n\n // 通知所有 worker\n for (const id in cluster.workers) {\n cluster.workers[id]?.send({ type: 'shutdown' });\n }\n\n // 等待 worker 退出 or 强杀\n await new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n for (const id in cluster.workers) {\n cluster.workers[id]?.kill('SIGKILL');\n }\n resolve();\n }, 5000);\n (cluster as any).on('exit', () => {\n if (Object.keys(cluster?.workers || {}).length === 0) {\n clearTimeout(timer);\n resolve();\n }\n });\n });\n\n process.exit(0);\n },\n};\n"],"mappings":";;;;;;AAGA,MAAM,WAAW;CACf,KAAK;CACL,eAAe;CAChB;AACD,MAAaA,eAAuB;CAClC,MAAM;CACN,SAAS;CACT,MAAM,KAAK;EACT,MAAM,YAAY,UAA8B,MAAM,QAAQ,MAAM,GAAG,MAAM,KAAK,OAAO,IAAI,GAAG,GAAG,GAAG,IAAI,MAAM;AAChH,MAAI,SAAS;GAEX,OAAO,KAAa,QAAQ,UAAU,QAAQ,IAAI,GAAG,SAAS,UAAU,UAAU,SAAS,MAAM,CAAC,EAAE,IAAI;GAExG,QAAQ,KAAa,QAAQ,UAAU,QAAQ,MAAM,GAAG,SAAS,UAAU,UAAU,SAAS,MAAM,CAAC,EAAE,IAAI;GAE3G,QAAQ,KAAa,QAAQ,UAAU,QAAQ,MAAM,GAAG,SAAS,UAAU,UAAU,SAAS,MAAM,CAAC,EAAE,IAAI;GAE3G,OAAO,KAAa,QAAQ,UAAU,QAAQ,KAAK,GAAG,SAAS,UAAU,UAAU,SAAS,MAAM,CAAC,EAAE,IAAI;GAC1G;;CAEJ;;;;AClBD,MAAM,SAAS,OAAO,QAAQ,eAAe,IAAI,IAAI,aAAa;AAElE,IAAI,eAAe;AACnB,IAAI,YAAY;AAChB,MAAaC,eAAuB;CAClC,MAAM;CACN,SAAS;CACT,IAAI,KAAK;AACP,cAAY,QAAQ;EACpB,MAAM,EAAE,SAAS,kBAAkB,IAAI,OAAO;AAC9C,YAAU,OAAO,kBAAkB,UAAU,kCAAkC;EAC/E,MAAM,WAAW,GAAG,MAAM,CAAC;EAC3B,MAAM,UAAU,kBAAkB,IAAI,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,UAAU,cAAc,CAAC;AAC/F,MAAI,aAAa,UAAU,GAAG;AAC5B,QAAK,IAAI,IAAI,GAAG,IAAI,SAAS,IAC3B,SAAQ,MAAM;AAGhB,GAAC,QAAgB,GAAG,cAAc;AAChC,QAAI,aAAc;AAClB,YAAQ,MAAM;KACd;SACG;AACL,OAAI,SAAS,IAAI,MAAM;IACrB,MAAM,IAAI,OAAO,OAAO;IACxB,UAAU,IAAI,OAAO,OAAO;IAC5B,WAAW;IACX,QAAQ,IAAI;IACb,CAAC;AAEF,WAAQ,GAAG,WAAW,OAAO,QAA0B;AACrD,QAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,IAAI,SAAS,YAAY;AACtE,WAAM,IAAI,QAAQ,QAAQ;AAC1B,aAAQ,KAAK,EAAE;;KAEjB;;;CAGN,MAAM,MAAM,KAAK;AACf,MAAI,QAAQ;AAEV,SAAM,IAAI,QAAQ,QAAQ;AAC1B;;AAGF,MAAI,CAAC,WAAW;AACd,SAAM,IAAI,QAAQ,QAAQ;AAC1B,WAAQ,KAAK,EAAE;;AAGjB,MAAI,aAAc;AAClB,iBAAe;AAGf,OAAK,MAAM,MAAM,QAAQ,QACvB,SAAQ,QAAQ,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAIjD,QAAM,IAAI,SAAe,YAAY;GACnC,MAAM,QAAQ,iBAAiB;AAC7B,SAAK,MAAM,MAAM,QAAQ,QACvB,SAAQ,QAAQ,KAAK,KAAK,UAAU;AAEtC,aAAS;MACR,IAAK;AACR,GAAC,QAAgB,GAAG,cAAc;AAChC,QAAI,OAAO,KAAK,SAAS,WAAW,EAAE,CAAC,CAAC,WAAW,GAAG;AACpD,kBAAa,MAAM;AACnB,cAAS;;KAEX;IACF;AAEF,UAAQ,KAAK,EAAE;;CAElB"}
1
+ {"version":3,"file":"index.mjs","names":["LoggerPlugin: Plugin","ServerPlugin: Plugin"],"sources":["../src/logger.ts","../src/server.ts"],"sourcesContent":["import { type Plugin, type Application, type LogScope, LOG_COLORS, type LogLevel } from '@thinkbun/core';\nimport pc from 'picocolors';\n\n/**\n * 生成作用域前缀\n * @param scope - 日志作用域\n * @returns 格式化的作用域前缀\n */\nconst generateScopePrefix = (scope: LogScope | LogScope[]): string => {\n if (Array.isArray(scope)) {\n return scope.map((item) => `[${item}]`).join('');\n }\n return `[${scope}]`;\n};\n\n/**\n * 获取日志颜色函数\n * @param scope - 日志作用域\n * @returns 颜色函数\n */\nconst getLogColor = (scope: LogScope) => {\n const colorName = LOG_COLORS[scope as keyof typeof LOG_COLORS] || 'yellow';\n return pc[colorName];\n};\n\n/**\n * 创建统一的日志方法\n * @param level - 日志级别\n * @returns 日志方法函数\n */\nconst createLogMethod = (level: LogLevel) => {\n return (msg: string, scope: LogScope = 'app'): void => {\n const consoleMethod =\n level === 'info'\n ? console.log\n : level === 'error'\n ? console.error\n : level === 'debug'\n ? console.debug\n : console.warn;\n\n const scopePrefix = generateScopePrefix(scope);\n const colorFn = getLogColor(scope);\n\n consoleMethod(colorFn(scopePrefix), msg);\n };\n};\n\n/**\n * 日志插件,为应用添加统一的日志功能\n * 支持不同作用域的彩色日志输出\n */\nexport const LoggerPlugin: Plugin = {\n name: 'LoggerPlugin',\n enforce: 'pre',\n setup(app: Application) {\n app.logger = {\n info: createLogMethod('info'),\n error: createLogMethod('error'),\n debug: createLogMethod('debug'),\n warn: createLogMethod('warn'),\n };\n },\n};\n","import cluster from 'node:cluster';\nimport os from 'node:os';\n\nimport { invariant, type Plugin, type Application } from '@thinkbun/core';\n\n/**\n * 集群关闭超时时间(毫秒)\n */\nconst CLUSTER_SHUTDOWN_TIMEOUT = 5000;\n\n/**\n * 集群消息类型\n */\ninterface ClusterMessage {\n type: 'shutdown';\n}\n\n/**\n * 服务器配置接口\n */\ninterface ServerConfig {\n workers: number;\n port: number;\n host: string;\n}\n\n/**\n * 检查是否为测试环境\n */\nconst isTestEnvironment = (): boolean => {\n return typeof Bun !== 'undefined' && Bun.env.NODE_ENV === 'test';\n};\n\n/**\n * 计算合适的工作进程数量\n * @param configWorkers - 配置中的工作进程数量\n * @returns 实际使用的工作进程数量\n */\nconst calculateWorkerCount = (configWorkers: number): number => {\n const cpuCount = os.cpus().length;\n return configWorkers === 0 ? cpuCount : Math.max(1, Math.min(cpuCount, configWorkers));\n};\n\n/**\n * 启动工作进程\n * @param count - 工作进程数量\n */\nconst startWorkers = (count: number): void => {\n for (let i = 0; i < count; i++) {\n try {\n cluster.fork();\n } catch (error) {\n console.error(`Failed to fork worker ${i}:`, error);\n }\n }\n};\n\n/**\n * 设置工作进程重启逻辑\n * @param shuttingDownRef - 关闭状态引用\n */\nconst setupWorkerRestart = (shuttingDownRef: { value: boolean }): void => {\n (cluster as any).on('exit', (worker: any, code: number, signal: string) => {\n if (shuttingDownRef.value) {\n return;\n }\n\n console.log(`Worker ${worker.process.pid} died (${code}, ${signal}). Restarting...`);\n\n try {\n cluster.fork();\n } catch (error) {\n console.error('Failed to restart worker:', error);\n }\n });\n};\n\n/**\n * 启动 HTTP 服务器\n * @param app - 应用实例\n */\nconst startHttpServer = (app: Application): void => {\n try {\n app.server = Bun.serve({\n port: app.loader.config.port,\n hostname: app.loader.config.host,\n reusePort: true,\n routes: app.routes,\n });\n } catch (error) {\n console.error('Failed to start server:', error);\n process.exit(1);\n }\n};\n\n/**\n * 设置工作进程的消息处理\n * @param app - 应用实例\n */\nconst setupWorkerMessageHandler = (app: Application): void => {\n process.on('message', async (msg: ClusterMessage) => {\n if (typeof msg === 'object' && msg !== null && msg.type === 'shutdown') {\n try {\n await app.server?.stop?.();\n process.exit(0);\n } catch (error) {\n console.error('Error during worker shutdown:', error);\n process.exit(1);\n }\n }\n });\n};\n\n/**\n * 优雅关闭工作进程\n */\nconst gracefulShutdownWorkers = (): Promise<void> => {\n return new Promise<void>((resolve) => {\n const workers = cluster.workers || {};\n const workerCount = Object.keys(workers).length;\n\n if (workerCount === 0) {\n resolve();\n return;\n }\n\n let exitedWorkers = 0;\n\n // 设置强制关闭定时器\n const forceKillTimer = setTimeout(() => {\n console.log('Force killing remaining workers...');\n for (const id in workers) {\n try {\n workers[id]?.kill('SIGKILL');\n } catch (error) {\n console.error(`Failed to kill worker ${id}:`, error);\n }\n }\n resolve();\n }, CLUSTER_SHUTDOWN_TIMEOUT);\n\n // 监听工作进程退出\n const onWorkerExit = () => {\n exitedWorkers++;\n if (exitedWorkers >= workerCount) {\n clearTimeout(forceKillTimer);\n resolve();\n }\n };\n\n (cluster as any).on('exit', onWorkerExit);\n\n // 发送关闭信号\n for (const id in workers) {\n try {\n workers[id]?.send({ type: 'shutdown' });\n } catch (error) {\n console.error(`Failed to send shutdown message to worker ${id}:`, error);\n // 如果发送失败,直接视为已退出\n exitedWorkers++;\n }\n }\n\n // 检查是否所有工作进程都已经退出\n if (exitedWorkers >= workerCount) {\n clearTimeout(forceKillTimer);\n (cluster as any).removeListener('exit', onWorkerExit);\n resolve();\n }\n });\n};\n\n/**\n * 服务器插件,提供集群管理和 HTTP 服务功能\n * 支持多进程模式和工作进程自动重启\n */\nexport const ServerPlugin: Plugin = {\n name: 'ServerPlugin',\n enforce: 'post',\n\n /**\n * 插件运行逻辑\n * @param app - 应用实例\n */\n run(app: Application) {\n const isPrimaryProcess = cluster.isPrimary;\n const config = app.loader.config as ServerConfig;\n\n // 验证配置\n invariant(typeof config.workers === 'number', 'config.workers must be a number');\n\n const workerCount = calculateWorkerCount(config.workers);\n const shuttingDownRef = { value: false };\n\n if (isPrimaryProcess && workerCount > 1) {\n // 主进程:启动工作进程\n console.log(`Starting ${workerCount} workers...`);\n startWorkers(workerCount);\n setupWorkerRestart(shuttingDownRef);\n } else {\n // 工作进程或单进程模式:启动 HTTP 服务器\n startHttpServer(app);\n setupWorkerMessageHandler(app);\n }\n\n // 保存状态供关闭时使用\n (this as any).isPrimary = isPrimaryProcess;\n (this as any).shuttingDownRef = shuttingDownRef;\n },\n\n /**\n * 插件关闭逻辑\n * @param app - 应用实例\n */\n async close(app: Application) {\n const isTest = isTestEnvironment();\n const isPrimary = (this as any).isPrimary;\n const shuttingDownRef = (this as any).shuttingDownRef || { value: false };\n\n if (isTest) {\n // 测试环境:只关闭服务器,不退出进程\n try {\n await app.server?.stop?.();\n } catch (error) {\n console.error('Error stopping server in test mode:', error);\n }\n return;\n }\n\n // 非主进程工作节点:关闭服务器并退出\n if (!isPrimary) {\n try {\n await app.server?.stop?.();\n } catch (error) {\n console.error('Error stopping worker server:', error);\n }\n process.exit(0);\n }\n\n // 主进程:优雅关闭所有工作进程\n if (shuttingDownRef.value) {\n return;\n }\n\n shuttingDownRef.value = true;\n console.log('Shutting down server cluster...');\n\n try {\n await gracefulShutdownWorkers();\n console.log('All workers shut down successfully');\n } catch (error) {\n console.error('Error during cluster shutdown:', error);\n }\n\n process.exit(0);\n },\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkbun/plugin",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.mjs",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "picocolors": "^1.1.1",
23
- "@thinkbun/core": "^1.0.0"
23
+ "@thinkbun/core": "^1.0.4"
24
24
  },
25
25
  "files": [
26
26
  "dist"