@dangao/bun-server 1.12.0 → 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.
Files changed (223) hide show
  1. package/README.md +32 -0
  2. package/dist/ai/ai-module.d.ts +24 -0
  3. package/dist/ai/ai-module.d.ts.map +1 -0
  4. package/dist/ai/decorators.d.ts +25 -0
  5. package/dist/ai/decorators.d.ts.map +1 -0
  6. package/dist/ai/errors.d.ts +39 -0
  7. package/dist/ai/errors.d.ts.map +1 -0
  8. package/dist/ai/index.d.ts +12 -0
  9. package/dist/ai/index.d.ts.map +1 -0
  10. package/dist/ai/providers/anthropic-provider.d.ts +23 -0
  11. package/dist/ai/providers/anthropic-provider.d.ts.map +1 -0
  12. package/dist/ai/providers/google-provider.d.ts +20 -0
  13. package/dist/ai/providers/google-provider.d.ts.map +1 -0
  14. package/dist/ai/providers/ollama-provider.d.ts +17 -0
  15. package/dist/ai/providers/ollama-provider.d.ts.map +1 -0
  16. package/dist/ai/providers/openai-provider.d.ts +28 -0
  17. package/dist/ai/providers/openai-provider.d.ts.map +1 -0
  18. package/dist/ai/service.d.ts +40 -0
  19. package/dist/ai/service.d.ts.map +1 -0
  20. package/dist/ai/tools/tool-executor.d.ts +15 -0
  21. package/dist/ai/tools/tool-executor.d.ts.map +1 -0
  22. package/dist/ai/tools/tool-registry.d.ts +39 -0
  23. package/dist/ai/tools/tool-registry.d.ts.map +1 -0
  24. package/dist/ai/types.d.ts +134 -0
  25. package/dist/ai/types.d.ts.map +1 -0
  26. package/dist/ai-guard/ai-guard-module.d.ts +18 -0
  27. package/dist/ai-guard/ai-guard-module.d.ts.map +1 -0
  28. package/dist/ai-guard/decorators.d.ts +16 -0
  29. package/dist/ai-guard/decorators.d.ts.map +1 -0
  30. package/dist/ai-guard/detectors/content-moderator.d.ts +26 -0
  31. package/dist/ai-guard/detectors/content-moderator.d.ts.map +1 -0
  32. package/dist/ai-guard/detectors/injection-detector.d.ts +13 -0
  33. package/dist/ai-guard/detectors/injection-detector.d.ts.map +1 -0
  34. package/dist/ai-guard/detectors/pii-detector.d.ts +11 -0
  35. package/dist/ai-guard/detectors/pii-detector.d.ts.map +1 -0
  36. package/dist/ai-guard/index.d.ts +8 -0
  37. package/dist/ai-guard/index.d.ts.map +1 -0
  38. package/dist/ai-guard/service.d.ts +21 -0
  39. package/dist/ai-guard/service.d.ts.map +1 -0
  40. package/dist/ai-guard/types.d.ts +59 -0
  41. package/dist/ai-guard/types.d.ts.map +1 -0
  42. package/dist/conversation/conversation-module.d.ts +25 -0
  43. package/dist/conversation/conversation-module.d.ts.map +1 -0
  44. package/dist/conversation/decorators.d.ts +28 -0
  45. package/dist/conversation/decorators.d.ts.map +1 -0
  46. package/dist/conversation/index.d.ts +8 -0
  47. package/dist/conversation/index.d.ts.map +1 -0
  48. package/dist/conversation/service.d.ts +43 -0
  49. package/dist/conversation/service.d.ts.map +1 -0
  50. package/dist/conversation/stores/database-store.d.ts +46 -0
  51. package/dist/conversation/stores/database-store.d.ts.map +1 -0
  52. package/dist/conversation/stores/memory-store.d.ts +17 -0
  53. package/dist/conversation/stores/memory-store.d.ts.map +1 -0
  54. package/dist/conversation/stores/redis-store.d.ts +39 -0
  55. package/dist/conversation/stores/redis-store.d.ts.map +1 -0
  56. package/dist/conversation/types.d.ts +64 -0
  57. package/dist/conversation/types.d.ts.map +1 -0
  58. package/dist/core/cluster.d.ts +42 -3
  59. package/dist/core/cluster.d.ts.map +1 -1
  60. package/dist/core/index.d.ts +1 -1
  61. package/dist/core/index.d.ts.map +1 -1
  62. package/dist/core/server.d.ts.map +1 -1
  63. package/dist/embedding/embedding-module.d.ts +20 -0
  64. package/dist/embedding/embedding-module.d.ts.map +1 -0
  65. package/dist/embedding/index.d.ts +6 -0
  66. package/dist/embedding/index.d.ts.map +1 -0
  67. package/dist/embedding/providers/ollama-embedding-provider.d.ts +18 -0
  68. package/dist/embedding/providers/ollama-embedding-provider.d.ts.map +1 -0
  69. package/dist/embedding/providers/openai-embedding-provider.d.ts +18 -0
  70. package/dist/embedding/providers/openai-embedding-provider.d.ts.map +1 -0
  71. package/dist/embedding/service.d.ts +27 -0
  72. package/dist/embedding/service.d.ts.map +1 -0
  73. package/dist/embedding/types.d.ts +25 -0
  74. package/dist/embedding/types.d.ts.map +1 -0
  75. package/dist/index.d.ts +9 -1
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +2870 -88
  78. package/dist/mcp/decorators.d.ts +42 -0
  79. package/dist/mcp/decorators.d.ts.map +1 -0
  80. package/dist/mcp/index.d.ts +6 -0
  81. package/dist/mcp/index.d.ts.map +1 -0
  82. package/dist/mcp/mcp-module.d.ts +22 -0
  83. package/dist/mcp/mcp-module.d.ts.map +1 -0
  84. package/dist/mcp/registry.d.ts +23 -0
  85. package/dist/mcp/registry.d.ts.map +1 -0
  86. package/dist/mcp/server.d.ts +29 -0
  87. package/dist/mcp/server.d.ts.map +1 -0
  88. package/dist/mcp/types.d.ts +60 -0
  89. package/dist/mcp/types.d.ts.map +1 -0
  90. package/dist/prompt/index.d.ts +6 -0
  91. package/dist/prompt/index.d.ts.map +1 -0
  92. package/dist/prompt/prompt-module.d.ts +23 -0
  93. package/dist/prompt/prompt-module.d.ts.map +1 -0
  94. package/dist/prompt/service.d.ts +47 -0
  95. package/dist/prompt/service.d.ts.map +1 -0
  96. package/dist/prompt/stores/file-store.d.ts +36 -0
  97. package/dist/prompt/stores/file-store.d.ts.map +1 -0
  98. package/dist/prompt/stores/memory-store.d.ts +17 -0
  99. package/dist/prompt/stores/memory-store.d.ts.map +1 -0
  100. package/dist/prompt/types.d.ts +68 -0
  101. package/dist/prompt/types.d.ts.map +1 -0
  102. package/dist/rag/chunkers/markdown-chunker.d.ts +11 -0
  103. package/dist/rag/chunkers/markdown-chunker.d.ts.map +1 -0
  104. package/dist/rag/chunkers/text-chunker.d.ts +11 -0
  105. package/dist/rag/chunkers/text-chunker.d.ts.map +1 -0
  106. package/dist/rag/decorators.d.ts +24 -0
  107. package/dist/rag/decorators.d.ts.map +1 -0
  108. package/dist/rag/index.d.ts +7 -0
  109. package/dist/rag/index.d.ts.map +1 -0
  110. package/dist/rag/rag-module.d.ts +23 -0
  111. package/dist/rag/rag-module.d.ts.map +1 -0
  112. package/dist/rag/service.d.ts +36 -0
  113. package/dist/rag/service.d.ts.map +1 -0
  114. package/dist/rag/types.d.ts +56 -0
  115. package/dist/rag/types.d.ts.map +1 -0
  116. package/dist/vector-store/index.d.ts +6 -0
  117. package/dist/vector-store/index.d.ts.map +1 -0
  118. package/dist/vector-store/stores/memory-store.d.ts +17 -0
  119. package/dist/vector-store/stores/memory-store.d.ts.map +1 -0
  120. package/dist/vector-store/stores/pinecone-store.d.ts +27 -0
  121. package/dist/vector-store/stores/pinecone-store.d.ts.map +1 -0
  122. package/dist/vector-store/stores/qdrant-store.d.ts +29 -0
  123. package/dist/vector-store/stores/qdrant-store.d.ts.map +1 -0
  124. package/dist/vector-store/types.d.ts +60 -0
  125. package/dist/vector-store/types.d.ts.map +1 -0
  126. package/dist/vector-store/vector-store-module.d.ts +20 -0
  127. package/dist/vector-store/vector-store-module.d.ts.map +1 -0
  128. package/docs/ai.md +500 -0
  129. package/docs/best-practices.md +83 -8
  130. package/docs/database.md +23 -0
  131. package/docs/guide.md +90 -27
  132. package/docs/migration.md +81 -7
  133. package/docs/security.md +23 -0
  134. package/docs/zh/ai.md +441 -0
  135. package/docs/zh/best-practices.md +43 -0
  136. package/docs/zh/database.md +23 -0
  137. package/docs/zh/guide.md +40 -1
  138. package/docs/zh/migration.md +39 -0
  139. package/docs/zh/security.md +23 -0
  140. package/package.json +2 -2
  141. package/src/ai/ai-module.ts +62 -0
  142. package/src/ai/decorators.ts +30 -0
  143. package/src/ai/errors.ts +71 -0
  144. package/src/ai/index.ts +11 -0
  145. package/src/ai/providers/anthropic-provider.ts +190 -0
  146. package/src/ai/providers/google-provider.ts +179 -0
  147. package/src/ai/providers/ollama-provider.ts +126 -0
  148. package/src/ai/providers/openai-provider.ts +242 -0
  149. package/src/ai/service.ts +155 -0
  150. package/src/ai/tools/tool-executor.ts +38 -0
  151. package/src/ai/tools/tool-registry.ts +91 -0
  152. package/src/ai/types.ts +145 -0
  153. package/src/ai-guard/ai-guard-module.ts +50 -0
  154. package/src/ai-guard/decorators.ts +21 -0
  155. package/src/ai-guard/detectors/content-moderator.ts +80 -0
  156. package/src/ai-guard/detectors/injection-detector.ts +48 -0
  157. package/src/ai-guard/detectors/pii-detector.ts +64 -0
  158. package/src/ai-guard/index.ts +7 -0
  159. package/src/ai-guard/service.ts +100 -0
  160. package/src/ai-guard/types.ts +61 -0
  161. package/src/conversation/conversation-module.ts +63 -0
  162. package/src/conversation/decorators.ts +47 -0
  163. package/src/conversation/index.ts +7 -0
  164. package/src/conversation/service.ts +133 -0
  165. package/src/conversation/stores/database-store.ts +125 -0
  166. package/src/conversation/stores/memory-store.ts +57 -0
  167. package/src/conversation/stores/redis-store.ts +101 -0
  168. package/src/conversation/types.ts +68 -0
  169. package/src/core/cluster.ts +239 -46
  170. package/src/core/index.ts +1 -1
  171. package/src/core/server.ts +91 -78
  172. package/src/embedding/embedding-module.ts +52 -0
  173. package/src/embedding/index.ts +5 -0
  174. package/src/embedding/providers/ollama-embedding-provider.ts +39 -0
  175. package/src/embedding/providers/openai-embedding-provider.ts +47 -0
  176. package/src/embedding/service.ts +55 -0
  177. package/src/embedding/types.ts +27 -0
  178. package/src/index.ts +11 -1
  179. package/src/mcp/decorators.ts +60 -0
  180. package/src/mcp/index.ts +5 -0
  181. package/src/mcp/mcp-module.ts +58 -0
  182. package/src/mcp/registry.ts +72 -0
  183. package/src/mcp/server.ts +164 -0
  184. package/src/mcp/types.ts +63 -0
  185. package/src/prompt/index.ts +5 -0
  186. package/src/prompt/prompt-module.ts +61 -0
  187. package/src/prompt/service.ts +93 -0
  188. package/src/prompt/stores/file-store.ts +135 -0
  189. package/src/prompt/stores/memory-store.ts +82 -0
  190. package/src/prompt/types.ts +84 -0
  191. package/src/rag/chunkers/markdown-chunker.ts +40 -0
  192. package/src/rag/chunkers/text-chunker.ts +30 -0
  193. package/src/rag/decorators.ts +26 -0
  194. package/src/rag/index.ts +6 -0
  195. package/src/rag/rag-module.ts +78 -0
  196. package/src/rag/service.ts +134 -0
  197. package/src/rag/types.ts +47 -0
  198. package/src/vector-store/index.ts +5 -0
  199. package/src/vector-store/stores/memory-store.ts +69 -0
  200. package/src/vector-store/stores/pinecone-store.ts +123 -0
  201. package/src/vector-store/stores/qdrant-store.ts +147 -0
  202. package/src/vector-store/types.ts +77 -0
  203. package/src/vector-store/vector-store-module.ts +50 -0
  204. package/tests/ai/ai-module.test.ts +46 -0
  205. package/tests/ai/ai-service.test.ts +91 -0
  206. package/tests/ai/tool-registry.test.ts +57 -0
  207. package/tests/ai-guard/ai-guard-module.test.ts +23 -0
  208. package/tests/ai-guard/content-moderator.test.ts +65 -0
  209. package/tests/ai-guard/pii-detector.test.ts +41 -0
  210. package/tests/conversation/conversation-module.test.ts +26 -0
  211. package/tests/conversation/conversation-service.test.ts +64 -0
  212. package/tests/conversation/memory-store.test.ts +68 -0
  213. package/tests/core/cluster.test.ts +45 -1
  214. package/tests/embedding/embedding-service.test.ts +55 -0
  215. package/tests/mcp/mcp-server.test.ts +85 -0
  216. package/tests/prompt/prompt-module.test.ts +30 -0
  217. package/tests/prompt/prompt-service.test.ts +74 -0
  218. package/tests/rag/chunkers.test.ts +58 -0
  219. package/tests/rag/rag-service.test.ts +66 -0
  220. package/tests/vector-store/memory-vector-store.test.ts +84 -0
  221. package/tests/interceptor/perf/interceptor-performance.test.ts +0 -340
  222. package/tests/perf/optimization.test.ts +0 -182
  223. package/tests/perf/regression.test.ts +0 -120
@@ -1,5 +1,16 @@
1
1
  import { spawn } from 'bun';
2
2
  import { LoggerManager } from '@dangao/logsmith';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import { mkdirSync, rmSync, existsSync } from 'fs';
6
+
7
+ /**
8
+ * 集群模式
9
+ * - 'reusePort': 使用 SO_REUSEPORT 内核分发(仅 Linux 有效)
10
+ * - 'proxy': 主进程通过 Unix socket round-robin 代理转发(跨平台,有额外开销)
11
+ * - 'auto': 默认使用 reusePort
12
+ */
13
+ export type ClusterMode = 'reusePort' | 'proxy' | 'auto';
3
14
 
4
15
  export interface ClusterOptions {
5
16
  /**
@@ -12,11 +23,18 @@ export interface ClusterOptions {
12
23
  * @default 'auto'
13
24
  */
14
25
  workers?: number | 'auto';
26
+ /**
27
+ * 集群模式
28
+ * @default 'auto'
29
+ */
30
+ mode?: ClusterMode;
15
31
  }
16
32
 
17
33
  /**
18
34
  * 集群管理器
19
- * 自动派生 worker 进程,每个 worker 使用 reusePort 绑定相同端口
35
+ * 自动派生 worker 进程,支持两种模式:
36
+ * - reusePort:每个 worker 使用 SO_REUSEPORT 绑定相同端口(Linux 内核分发)
37
+ * - proxy:主进程 round-robin 代理转发到 worker 随机端口(跨平台)
20
38
  */
21
39
  export class ClusterManager {
22
40
  private readonly workerCount: number;
@@ -24,52 +42,48 @@ export class ClusterManager {
24
42
  private readonly scriptPath: string;
25
43
  private readonly port: number;
26
44
  private readonly hostname?: string;
45
+ private readonly mode: 'reusePort' | 'proxy';
46
+ private proxyServer?: ReturnType<typeof Bun.serve>;
47
+ private socketPaths: string[] = [];
48
+ private roundRobinIndex = 0;
49
+ private socketDir?: string;
27
50
 
28
51
  public constructor(options: {
29
52
  workers: number | 'auto';
30
53
  scriptPath: string;
31
54
  port: number;
32
55
  hostname?: string;
56
+ mode?: ClusterMode;
33
57
  }) {
34
58
  this.workerCount =
35
59
  options.workers === 'auto' ? navigator.hardwareConcurrency : options.workers;
36
60
  this.scriptPath = options.scriptPath;
37
61
  this.port = options.port;
38
62
  this.hostname = options.hostname;
63
+
64
+ const requestedMode = options.mode ?? 'auto';
65
+ this.mode = requestedMode === 'auto' ? 'reusePort' : requestedMode;
39
66
  }
40
67
 
41
68
  /**
42
- * 启动所有 worker 进程
69
+ * 获取已解析的集群模式
43
70
  */
44
- public start(): void {
45
- const logger = LoggerManager.getLogger();
46
- logger.info(
47
- `[Cluster] Starting ${this.workerCount} workers on port ${this.port}`,
48
- );
71
+ public getMode(): 'reusePort' | 'proxy' {
72
+ return this.mode;
73
+ }
49
74
 
50
- for (let i = 0; i < this.workerCount; i++) {
51
- const worker = spawn({
52
- cmd: ['bun', 'run', this.scriptPath],
53
- env: {
54
- ...process.env,
55
- PORT: String(this.port),
56
- REUSE_PORT: '1',
57
- CLUSTER_WORKER: '1',
58
- WORKER_ID: String(i),
59
- ...(this.hostname ? { HOSTNAME: this.hostname } : {}),
60
- },
61
- stdout: 'inherit',
62
- stderr: 'inherit',
63
- });
64
- this.workers.push(worker);
75
+ /**
76
+ * 启动所有 worker 进程
77
+ *
78
+ * - reusePort 模式:立即返回(workers 异步启动)
79
+ * - proxy 模式:等待所有 worker 就绪后启动代理服务器再返回
80
+ */
81
+ public async start(): Promise<void> {
82
+ if (this.mode === 'reusePort') {
83
+ this.startReusePort();
84
+ } else {
85
+ await this.startProxy();
65
86
  }
66
-
67
- logger.info(
68
- `[Cluster] ${this.workerCount} workers started (reusePort mode)`,
69
- );
70
-
71
- // Monitor workers and auto-restart on crash
72
- this.monitorWorkers();
73
87
  }
74
88
 
75
89
  /**
@@ -79,11 +93,15 @@ export class ClusterManager {
79
93
  const logger = LoggerManager.getLogger();
80
94
  logger.info('[Cluster] Stopping all workers...');
81
95
 
96
+ if (this.proxyServer) {
97
+ this.proxyServer.stop();
98
+ this.proxyServer = undefined;
99
+ }
100
+
82
101
  for (const worker of this.workers) {
83
102
  worker.kill('SIGTERM');
84
103
  }
85
104
 
86
- // Wait up to 5 seconds for workers to exit
87
105
  const timeout = setTimeout(() => {
88
106
  for (const worker of this.workers) {
89
107
  worker.kill('SIGKILL');
@@ -94,39 +112,205 @@ export class ClusterManager {
94
112
  clearTimeout(timeout);
95
113
 
96
114
  this.workers.length = 0;
115
+ this.socketPaths.length = 0;
116
+
117
+ if (this.socketDir) {
118
+ try {
119
+ rmSync(this.socketDir, { recursive: true, force: true });
120
+ } catch {
121
+ // ignore cleanup errors
122
+ }
123
+ this.socketDir = undefined;
124
+ }
125
+
97
126
  logger.info('[Cluster] All workers stopped');
98
127
  }
99
128
 
100
- private monitorWorkers(): void {
101
- // For each worker, restart if it crashes unexpectedly
129
+ // ── reusePort mode ──────────────────────────────────────────────
130
+
131
+ private startReusePort(): void {
132
+ const logger = LoggerManager.getLogger();
133
+ logger.info(
134
+ `[Cluster] Starting ${this.workerCount} workers on port ${this.port}`,
135
+ );
136
+
137
+ for (let i = 0; i < this.workerCount; i++) {
138
+ this.workers.push(this.spawnReusePortWorker(i));
139
+ }
140
+
141
+ logger.info(
142
+ `[Cluster] ${this.workerCount} workers started (reusePort mode)`,
143
+ );
144
+
145
+ this.monitorReusePortWorkers();
146
+ }
147
+
148
+ private spawnReusePortWorker(index: number): ReturnType<typeof spawn> {
149
+ return spawn({
150
+ cmd: ['bun', 'run', this.scriptPath],
151
+ env: {
152
+ ...process.env,
153
+ PORT: String(this.port),
154
+ REUSE_PORT: '1',
155
+ CLUSTER_WORKER: '1',
156
+ WORKER_ID: String(index),
157
+ ...(this.hostname ? { HOSTNAME: this.hostname } : {}),
158
+ },
159
+ stdout: 'inherit',
160
+ stderr: 'inherit',
161
+ });
162
+ }
163
+
164
+ private monitorReusePortWorkers(): void {
102
165
  for (let i = 0; i < this.workers.length; i++) {
103
166
  const index = i;
104
- this.workers[index].exited.then((exitCode) => {
167
+ this.workers[index]!.exited.then((exitCode) => {
105
168
  if (exitCode !== 0 && exitCode !== null) {
106
169
  const logger = LoggerManager.getLogger();
107
170
  logger.warn(
108
171
  `[Cluster] Worker ${index} exited with code ${exitCode}, restarting...`,
109
172
  );
173
+ this.workers[index] = this.spawnReusePortWorker(index);
174
+ }
175
+ });
176
+ }
177
+ }
110
178
 
111
- const newWorker = spawn({
112
- cmd: ['bun', 'run', this.scriptPath],
113
- env: {
114
- ...process.env,
115
- PORT: String(this.port),
116
- REUSE_PORT: '1',
117
- CLUSTER_WORKER: '1',
118
- WORKER_ID: String(index),
119
- ...(this.hostname ? { HOSTNAME: this.hostname } : {}),
120
- },
121
- stdout: 'inherit',
122
- stderr: 'inherit',
179
+ // ── proxy mode (Unix socket) ─────────────────────────────────────
180
+
181
+ private async startProxy(): Promise<void> {
182
+ const logger = LoggerManager.getLogger();
183
+ logger.info(
184
+ `[Cluster] Starting ${this.workerCount} workers in proxy mode on port ${this.port}`,
185
+ );
186
+
187
+ this.socketDir = join(tmpdir(), `bun-cluster-${process.pid}`);
188
+ // Clean up stale directory from a previous run with the same PID
189
+ try { rmSync(this.socketDir, { recursive: true, force: true }); } catch { /* ignore */ }
190
+ mkdirSync(this.socketDir, { recursive: true });
191
+
192
+ this.socketPaths = [];
193
+ for (let i = 0; i < this.workerCount; i++) {
194
+ const socketPath = join(this.socketDir, `w${i}.sock`);
195
+ this.socketPaths.push(socketPath);
196
+ this.workers.push(this.spawnProxyWorker(i, socketPath));
197
+ }
198
+
199
+ await this.waitForWorkerSockets();
200
+ this.startProxyServer();
201
+
202
+ logger.info(
203
+ `[Cluster] ${this.workerCount} workers ready (proxy mode, unix sockets)`,
204
+ );
205
+
206
+ this.monitorProxyWorkers();
207
+ }
208
+
209
+ private spawnProxyWorker(index: number, socketPath: string): ReturnType<typeof spawn> {
210
+ return spawn({
211
+ cmd: ['bun', 'run', this.scriptPath],
212
+ env: {
213
+ ...process.env,
214
+ PORT: '0',
215
+ REUSE_PORT: '0',
216
+ CLUSTER_WORKER: '1',
217
+ WORKER_ID: String(index),
218
+ CLUSTER_MODE: 'proxy',
219
+ CLUSTER_SOCKET_FILE: socketPath,
220
+ ...(this.hostname ? { HOSTNAME: this.hostname } : {}),
221
+ },
222
+ stdout: 'inherit',
223
+ stderr: 'inherit',
224
+ });
225
+ }
226
+
227
+ private async waitForWorkerSockets(): Promise<void> {
228
+ const deadline = Date.now() + 30_000;
229
+
230
+ while (Date.now() < deadline) {
231
+ let allReady = true;
232
+
233
+ for (const socketPath of this.socketPaths) {
234
+ if (!existsSync(socketPath)) {
235
+ allReady = false;
236
+ break;
237
+ }
238
+ }
239
+
240
+ if (allReady) {
241
+ // Give workers a moment to finish bind+listen after file creation
242
+ await Bun.sleep(200);
243
+ return;
244
+ }
245
+
246
+ await Bun.sleep(100);
247
+ }
248
+
249
+ const ready = this.socketPaths.filter((p) => existsSync(p)).length;
250
+ throw new Error(
251
+ `[Cluster] Not all workers created sockets within 30s (got ${ready}/${this.workerCount})`,
252
+ );
253
+ }
254
+
255
+ private startProxyServer(): void {
256
+ const sockets = this.socketPaths;
257
+ const count = sockets.length;
258
+
259
+ this.proxyServer = Bun.serve({
260
+ port: this.port,
261
+ hostname: this.hostname,
262
+ fetch: async (req) => {
263
+ const socketPath = sockets[this.roundRobinIndex % count]!;
264
+ this.roundRobinIndex++;
265
+
266
+ try {
267
+ return await fetch(req.url, {
268
+ method: req.method,
269
+ headers: req.headers,
270
+ body: req.body,
271
+ redirect: 'manual',
272
+ unix: socketPath,
123
273
  });
124
- this.workers[index] = newWorker;
274
+ } catch {
275
+ return new Response('Bad Gateway', { status: 502 });
276
+ }
277
+ },
278
+ });
279
+ }
280
+
281
+ private monitorProxyWorkers(): void {
282
+ for (let i = 0; i < this.workers.length; i++) {
283
+ const index = i;
284
+ this.workers[index]!.exited.then(async (exitCode) => {
285
+ if (exitCode !== 0 && exitCode !== null) {
286
+ const logger = LoggerManager.getLogger();
287
+ logger.warn(
288
+ `[Cluster] Worker ${index} exited with code ${exitCode}, restarting...`,
289
+ );
290
+
291
+ const socketPath = this.socketPaths[index]!;
292
+ try { rmSync(socketPath); } catch { /* ignore */ }
293
+
294
+ this.workers[index] = this.spawnProxyWorker(index, socketPath);
295
+
296
+ const deadline = Date.now() + 15_000;
297
+ while (Date.now() < deadline) {
298
+ if (existsSync(socketPath)) {
299
+ await Bun.sleep(200);
300
+ logger.info(`[Cluster] Worker ${index} restarted (unix socket)`);
301
+ return;
302
+ }
303
+ await Bun.sleep(100);
304
+ }
305
+
306
+ logger.error(`[Cluster] Worker ${index} failed to restart within 15s`);
125
307
  }
126
308
  });
127
309
  }
128
310
  }
129
311
 
312
+ // ── static helpers ──────────────────────────────────────────────
313
+
130
314
  /**
131
315
  * 检查当前进程是否为 cluster worker
132
316
  */
@@ -140,4 +324,13 @@ export class ClusterManager {
140
324
  public static getWorkerId(): number {
141
325
  return Number(process.env.WORKER_ID ?? -1);
142
326
  }
327
+
328
+ /**
329
+ * 获取当前 worker 的集群模式
330
+ * @returns 集群模式,非 worker 进程返回 null
331
+ */
332
+ public static getClusterMode(): 'reusePort' | 'proxy' | null {
333
+ if (!ClusterManager.isWorker()) return null;
334
+ return process.env.CLUSTER_MODE === 'proxy' ? 'proxy' : 'reusePort';
335
+ }
143
336
  }
package/src/core/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { Application, type ApplicationOptions } from './application';
2
2
  export { BunServer, type ServerOptions } from './server';
3
- export { ClusterManager, type ClusterOptions } from './cluster';
3
+ export { ClusterManager, type ClusterOptions, type ClusterMode } from './cluster';
4
4
  export { Context } from './context';
5
5
  export { ContextService, CONTEXT_SERVICE_TOKEN, contextStore } from './context-service';
6
6
 
@@ -75,92 +75,105 @@ export class BunServer {
75
75
  this.shutdownPromise = undefined;
76
76
  this.shutdownResolve = undefined;
77
77
 
78
- this.server = Bun.serve({
79
- port: this.options.port ?? 3000,
80
- hostname: this.options.hostname,
81
- reusePort: this.options.reusePort,
82
- fetch: (
83
- request: Request,
84
- server: Server<WebSocketConnectionData>,
85
- ): Response | Promise<Response> | undefined => {
86
- // 如果正在关闭,拒绝新请求
87
- if (this.isShuttingDown) {
88
- return new Response("Server is shutting down", { status: 503 });
78
+ const fetchHandler = (
79
+ request: Request,
80
+ server: Server<WebSocketConnectionData>,
81
+ ): Response | Promise<Response> | undefined => {
82
+ if (this.isShuttingDown) {
83
+ return new Response("Server is shutting down", { status: 503 });
84
+ }
85
+
86
+ const upgradeHeader = request.headers.get("upgrade");
87
+ if (
88
+ this.options.websocketRegistry &&
89
+ upgradeHeader &&
90
+ upgradeHeader.toLowerCase() === "websocket"
91
+ ) {
92
+ const url = new URL(request.url);
93
+ if (!this.options.websocketRegistry.hasGateway(url.pathname)) {
94
+ return new Response("WebSocket gateway not found", { status: 404 });
89
95
  }
90
-
91
- const upgradeHeader = request.headers.get("upgrade");
92
- if (
93
- this.options.websocketRegistry &&
94
- upgradeHeader &&
95
- upgradeHeader.toLowerCase() === "websocket"
96
- ) {
97
- const url = new URL(request.url);
98
- // 检查是否有匹配的网关(支持动态路径匹配)
99
- if (!this.options.websocketRegistry.hasGateway(url.pathname)) {
100
- return new Response("WebSocket gateway not found", { status: 404 });
101
- }
102
- // 创建 Context 以便在 WebSocket 处理器中使用
103
- const context = new Context(request);
104
- // 创建 Bun 兼容的 URLSearchParams(需要 toJSON 方法)
105
- const queryParams = new URLSearchParams(url.searchParams);
106
- const upgraded = server.upgrade(request, {
107
- data: {
108
- path: url.pathname,
109
- query: queryParams,
110
- context,
111
- },
96
+ const context = new Context(request);
97
+ const queryParams = new URLSearchParams(url.searchParams);
98
+ const upgraded = server.upgrade(request, {
99
+ data: {
100
+ path: url.pathname,
101
+ query: queryParams,
102
+ context,
103
+ },
104
+ });
105
+ if (upgraded) {
106
+ return undefined;
107
+ }
108
+ return new Response("WebSocket upgrade failed", { status: 400 });
109
+ }
110
+
111
+ this.activeRequests++;
112
+
113
+ const context = new Context(request);
114
+ const responsePromise = this.options.fetch(context);
115
+
116
+ if (responsePromise instanceof Promise) {
117
+ responsePromise
118
+ .finally(() => {
119
+ this.activeRequests--;
120
+ if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
121
+ this.shutdownResolve();
122
+ }
123
+ })
124
+ .catch(() => {
125
+ // errors handled by middleware pipeline
112
126
  });
113
- if (upgraded) {
114
- return undefined;
115
- }
116
- return new Response("WebSocket upgrade failed", { status: 400 });
127
+ } else {
128
+ this.activeRequests--;
129
+ if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
130
+ this.shutdownResolve();
117
131
  }
132
+ }
118
133
 
119
- // 增加活跃请求计数
120
- this.activeRequests++;
121
-
122
- const context = new Context(request);
123
- const responsePromise = this.options.fetch(context);
124
-
125
- // 处理响应完成后的清理
126
- if (responsePromise instanceof Promise) {
127
- responsePromise
128
- .finally(() => {
129
- this.activeRequests--;
130
- // 如果正在关闭且没有活跃请求,触发关闭完成
131
- if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
132
- this.shutdownResolve();
133
- }
134
- })
135
- .catch(() => {
136
- // 错误已在中间件中处理,这里只负责计数
137
- });
138
- } else {
139
- // 同步响应
140
- this.activeRequests--;
141
- if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
142
- this.shutdownResolve();
143
- }
144
- }
134
+ return responsePromise;
135
+ };
145
136
 
146
- return responsePromise;
137
+ const websocketHandlers = {
138
+ open: async (ws: import("bun").ServerWebSocket<WebSocketConnectionData>) => {
139
+ await this.options.websocketRegistry?.handleOpen(ws);
147
140
  },
148
- websocket: {
149
- open: async (ws) => {
150
- await this.options.websocketRegistry?.handleOpen(ws);
151
- },
152
- message: async (ws, message) => {
153
- await this.options.websocketRegistry?.handleMessage(ws, message);
154
- },
155
- close: async (ws, code, reason) => {
156
- await this.options.websocketRegistry?.handleClose(ws, code, reason);
157
- },
141
+ message: async (ws: import("bun").ServerWebSocket<WebSocketConnectionData>, message: string | Buffer) => {
142
+ await this.options.websocketRegistry?.handleMessage(ws, message);
158
143
  },
159
- });
144
+ close: async (ws: import("bun").ServerWebSocket<WebSocketConnectionData>, code: number, reason: string) => {
145
+ await this.options.websocketRegistry?.handleClose(ws, code, reason);
146
+ },
147
+ };
148
+
149
+ const socketFile = process.env.CLUSTER_SOCKET_FILE;
160
150
 
161
- const hostname = this.options.hostname ?? "localhost";
162
- const port = this.server.port;
163
- logger.info(`Server started at http://${hostname}:${port}`);
151
+ if (socketFile) {
152
+ // Unix socket mode for cluster proxy workers
153
+ this.server = Bun.serve({
154
+ unix: socketFile,
155
+ fetch: fetchHandler,
156
+ websocket: websocketHandlers,
157
+ });
158
+ logger.info(`Server started at unix://${socketFile}`);
159
+ } else {
160
+ this.server = Bun.serve({
161
+ port: this.options.port ?? 3000,
162
+ hostname: this.options.hostname,
163
+ reusePort: this.options.reusePort,
164
+ fetch: fetchHandler,
165
+ websocket: websocketHandlers,
166
+ });
167
+ const hostname = this.options.hostname ?? "localhost";
168
+ const port = this.server.port;
169
+ logger.info(`Server started at http://${hostname}:${port}`);
170
+
171
+ // In proxy cluster mode (TCP fallback), report port to master
172
+ const portFile = process.env.CLUSTER_PORT_FILE;
173
+ if (portFile) {
174
+ Bun.write(portFile, String(port));
175
+ }
176
+ }
164
177
  }
165
178
 
166
179
  /**
@@ -0,0 +1,52 @@
1
+ import { Module, MODULE_METADATA_KEY } from '../di/module';
2
+ import type { ModuleProvider } from '../di/module';
3
+ import { EmbeddingService } from './service';
4
+ import {
5
+ EMBEDDING_SERVICE_TOKEN,
6
+ EMBEDDING_OPTIONS_TOKEN,
7
+ type EmbeddingModuleOptions,
8
+ } from './types';
9
+
10
+ @Module({ providers: [] })
11
+ export class EmbeddingModule {
12
+ /**
13
+ * Configure the embedding module.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * EmbeddingModule.forRoot({
18
+ * provider: {
19
+ * name: 'openai',
20
+ * provider: OpenAIEmbeddingProvider,
21
+ * config: { apiKey: process.env.OPENAI_API_KEY! },
22
+ * },
23
+ * });
24
+ * ```
25
+ */
26
+ public static forRoot(options: EmbeddingModuleOptions): typeof EmbeddingModule {
27
+ const service = new EmbeddingService(options);
28
+
29
+ const providers: ModuleProvider[] = [
30
+ { provide: EMBEDDING_OPTIONS_TOKEN, useValue: options },
31
+ { provide: EMBEDDING_SERVICE_TOKEN, useValue: service },
32
+ EmbeddingService,
33
+ ];
34
+
35
+ const existing = Reflect.getMetadata(MODULE_METADATA_KEY, EmbeddingModule) || {};
36
+ Reflect.defineMetadata(MODULE_METADATA_KEY, {
37
+ ...existing,
38
+ providers: [...(existing.providers || []), ...providers],
39
+ exports: [
40
+ ...(existing.exports || []),
41
+ EMBEDDING_SERVICE_TOKEN,
42
+ EmbeddingService,
43
+ ],
44
+ }, EmbeddingModule);
45
+
46
+ return EmbeddingModule;
47
+ }
48
+
49
+ public static reset(): void {
50
+ Reflect.deleteMetadata(MODULE_METADATA_KEY, EmbeddingModule);
51
+ }
52
+ }
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export * from './service';
3
+ export * from './embedding-module';
4
+ export * from './providers/openai-embedding-provider';
5
+ export * from './providers/ollama-embedding-provider';
@@ -0,0 +1,39 @@
1
+ import type { EmbeddingProvider } from '../types';
2
+ import { AiProviderError } from '../../ai/errors';
3
+
4
+ export interface OllamaEmbeddingProviderConfig {
5
+ baseUrl?: string;
6
+ /** Default: nomic-embed-text */
7
+ model?: string;
8
+ /** Dimensions depend on model. Default 768 for nomic-embed-text */
9
+ dimensions?: number;
10
+ }
11
+
12
+ export class OllamaEmbeddingProvider implements EmbeddingProvider {
13
+ public readonly name = 'ollama';
14
+ public readonly dimensions: number;
15
+ private readonly baseUrl: string;
16
+ private readonly model: string;
17
+
18
+ public constructor(config: OllamaEmbeddingProviderConfig = {}) {
19
+ this.baseUrl = (config.baseUrl ?? 'http://localhost:11434').replace(/\/$/, '');
20
+ this.model = config.model ?? 'nomic-embed-text';
21
+ this.dimensions = config.dimensions ?? 768;
22
+ }
23
+
24
+ public async embed(text: string): Promise<number[]> {
25
+ const res = await fetch(`${this.baseUrl}/api/embed`, {
26
+ method: 'POST',
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: JSON.stringify({ model: this.model, input: text }),
29
+ });
30
+
31
+ if (!res.ok) throw new AiProviderError(await res.text(), 'ollama-embedding', res.status);
32
+ const data = await res.json() as { embeddings: number[][] };
33
+ return data.embeddings[0]!;
34
+ }
35
+
36
+ public async embedBatch(texts: string[]): Promise<number[][]> {
37
+ return Promise.all(texts.map((t) => this.embed(t)));
38
+ }
39
+ }