@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.
- package/README.md +32 -0
- package/dist/ai/ai-module.d.ts +24 -0
- package/dist/ai/ai-module.d.ts.map +1 -0
- package/dist/ai/decorators.d.ts +25 -0
- package/dist/ai/decorators.d.ts.map +1 -0
- package/dist/ai/errors.d.ts +39 -0
- package/dist/ai/errors.d.ts.map +1 -0
- package/dist/ai/index.d.ts +12 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/providers/anthropic-provider.d.ts +23 -0
- package/dist/ai/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/ai/providers/google-provider.d.ts +20 -0
- package/dist/ai/providers/google-provider.d.ts.map +1 -0
- package/dist/ai/providers/ollama-provider.d.ts +17 -0
- package/dist/ai/providers/ollama-provider.d.ts.map +1 -0
- package/dist/ai/providers/openai-provider.d.ts +28 -0
- package/dist/ai/providers/openai-provider.d.ts.map +1 -0
- package/dist/ai/service.d.ts +40 -0
- package/dist/ai/service.d.ts.map +1 -0
- package/dist/ai/tools/tool-executor.d.ts +15 -0
- package/dist/ai/tools/tool-executor.d.ts.map +1 -0
- package/dist/ai/tools/tool-registry.d.ts +39 -0
- package/dist/ai/tools/tool-registry.d.ts.map +1 -0
- package/dist/ai/types.d.ts +134 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai-guard/ai-guard-module.d.ts +18 -0
- package/dist/ai-guard/ai-guard-module.d.ts.map +1 -0
- package/dist/ai-guard/decorators.d.ts +16 -0
- package/dist/ai-guard/decorators.d.ts.map +1 -0
- package/dist/ai-guard/detectors/content-moderator.d.ts +26 -0
- package/dist/ai-guard/detectors/content-moderator.d.ts.map +1 -0
- package/dist/ai-guard/detectors/injection-detector.d.ts +13 -0
- package/dist/ai-guard/detectors/injection-detector.d.ts.map +1 -0
- package/dist/ai-guard/detectors/pii-detector.d.ts +11 -0
- package/dist/ai-guard/detectors/pii-detector.d.ts.map +1 -0
- package/dist/ai-guard/index.d.ts +8 -0
- package/dist/ai-guard/index.d.ts.map +1 -0
- package/dist/ai-guard/service.d.ts +21 -0
- package/dist/ai-guard/service.d.ts.map +1 -0
- package/dist/ai-guard/types.d.ts +59 -0
- package/dist/ai-guard/types.d.ts.map +1 -0
- package/dist/conversation/conversation-module.d.ts +25 -0
- package/dist/conversation/conversation-module.d.ts.map +1 -0
- package/dist/conversation/decorators.d.ts +28 -0
- package/dist/conversation/decorators.d.ts.map +1 -0
- package/dist/conversation/index.d.ts +8 -0
- package/dist/conversation/index.d.ts.map +1 -0
- package/dist/conversation/service.d.ts +43 -0
- package/dist/conversation/service.d.ts.map +1 -0
- package/dist/conversation/stores/database-store.d.ts +46 -0
- package/dist/conversation/stores/database-store.d.ts.map +1 -0
- package/dist/conversation/stores/memory-store.d.ts +17 -0
- package/dist/conversation/stores/memory-store.d.ts.map +1 -0
- package/dist/conversation/stores/redis-store.d.ts +39 -0
- package/dist/conversation/stores/redis-store.d.ts.map +1 -0
- package/dist/conversation/types.d.ts +64 -0
- package/dist/conversation/types.d.ts.map +1 -0
- package/dist/core/cluster.d.ts +42 -3
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/embedding/embedding-module.d.ts +20 -0
- package/dist/embedding/embedding-module.d.ts.map +1 -0
- package/dist/embedding/index.d.ts +6 -0
- package/dist/embedding/index.d.ts.map +1 -0
- package/dist/embedding/providers/ollama-embedding-provider.d.ts +18 -0
- package/dist/embedding/providers/ollama-embedding-provider.d.ts.map +1 -0
- package/dist/embedding/providers/openai-embedding-provider.d.ts +18 -0
- package/dist/embedding/providers/openai-embedding-provider.d.ts.map +1 -0
- package/dist/embedding/service.d.ts +27 -0
- package/dist/embedding/service.d.ts.map +1 -0
- package/dist/embedding/types.d.ts +25 -0
- package/dist/embedding/types.d.ts.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2870 -88
- package/dist/mcp/decorators.d.ts +42 -0
- package/dist/mcp/decorators.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/mcp-module.d.ts +22 -0
- package/dist/mcp/mcp-module.d.ts.map +1 -0
- package/dist/mcp/registry.d.ts +23 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/server.d.ts +29 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/types.d.ts +60 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/prompt/index.d.ts +6 -0
- package/dist/prompt/index.d.ts.map +1 -0
- package/dist/prompt/prompt-module.d.ts +23 -0
- package/dist/prompt/prompt-module.d.ts.map +1 -0
- package/dist/prompt/service.d.ts +47 -0
- package/dist/prompt/service.d.ts.map +1 -0
- package/dist/prompt/stores/file-store.d.ts +36 -0
- package/dist/prompt/stores/file-store.d.ts.map +1 -0
- package/dist/prompt/stores/memory-store.d.ts +17 -0
- package/dist/prompt/stores/memory-store.d.ts.map +1 -0
- package/dist/prompt/types.d.ts +68 -0
- package/dist/prompt/types.d.ts.map +1 -0
- package/dist/rag/chunkers/markdown-chunker.d.ts +11 -0
- package/dist/rag/chunkers/markdown-chunker.d.ts.map +1 -0
- package/dist/rag/chunkers/text-chunker.d.ts +11 -0
- package/dist/rag/chunkers/text-chunker.d.ts.map +1 -0
- package/dist/rag/decorators.d.ts +24 -0
- package/dist/rag/decorators.d.ts.map +1 -0
- package/dist/rag/index.d.ts +7 -0
- package/dist/rag/index.d.ts.map +1 -0
- package/dist/rag/rag-module.d.ts +23 -0
- package/dist/rag/rag-module.d.ts.map +1 -0
- package/dist/rag/service.d.ts +36 -0
- package/dist/rag/service.d.ts.map +1 -0
- package/dist/rag/types.d.ts +56 -0
- package/dist/rag/types.d.ts.map +1 -0
- package/dist/vector-store/index.d.ts +6 -0
- package/dist/vector-store/index.d.ts.map +1 -0
- package/dist/vector-store/stores/memory-store.d.ts +17 -0
- package/dist/vector-store/stores/memory-store.d.ts.map +1 -0
- package/dist/vector-store/stores/pinecone-store.d.ts +27 -0
- package/dist/vector-store/stores/pinecone-store.d.ts.map +1 -0
- package/dist/vector-store/stores/qdrant-store.d.ts +29 -0
- package/dist/vector-store/stores/qdrant-store.d.ts.map +1 -0
- package/dist/vector-store/types.d.ts +60 -0
- package/dist/vector-store/types.d.ts.map +1 -0
- package/dist/vector-store/vector-store-module.d.ts +20 -0
- package/dist/vector-store/vector-store-module.d.ts.map +1 -0
- package/docs/ai.md +500 -0
- package/docs/best-practices.md +83 -8
- package/docs/database.md +23 -0
- package/docs/guide.md +90 -27
- package/docs/migration.md +81 -7
- package/docs/security.md +23 -0
- package/docs/zh/ai.md +441 -0
- package/docs/zh/best-practices.md +43 -0
- package/docs/zh/database.md +23 -0
- package/docs/zh/guide.md +40 -1
- package/docs/zh/migration.md +39 -0
- package/docs/zh/security.md +23 -0
- package/package.json +2 -2
- package/src/ai/ai-module.ts +62 -0
- package/src/ai/decorators.ts +30 -0
- package/src/ai/errors.ts +71 -0
- package/src/ai/index.ts +11 -0
- package/src/ai/providers/anthropic-provider.ts +190 -0
- package/src/ai/providers/google-provider.ts +179 -0
- package/src/ai/providers/ollama-provider.ts +126 -0
- package/src/ai/providers/openai-provider.ts +242 -0
- package/src/ai/service.ts +155 -0
- package/src/ai/tools/tool-executor.ts +38 -0
- package/src/ai/tools/tool-registry.ts +91 -0
- package/src/ai/types.ts +145 -0
- package/src/ai-guard/ai-guard-module.ts +50 -0
- package/src/ai-guard/decorators.ts +21 -0
- package/src/ai-guard/detectors/content-moderator.ts +80 -0
- package/src/ai-guard/detectors/injection-detector.ts +48 -0
- package/src/ai-guard/detectors/pii-detector.ts +64 -0
- package/src/ai-guard/index.ts +7 -0
- package/src/ai-guard/service.ts +100 -0
- package/src/ai-guard/types.ts +61 -0
- package/src/conversation/conversation-module.ts +63 -0
- package/src/conversation/decorators.ts +47 -0
- package/src/conversation/index.ts +7 -0
- package/src/conversation/service.ts +133 -0
- package/src/conversation/stores/database-store.ts +125 -0
- package/src/conversation/stores/memory-store.ts +57 -0
- package/src/conversation/stores/redis-store.ts +101 -0
- package/src/conversation/types.ts +68 -0
- package/src/core/cluster.ts +239 -46
- package/src/core/index.ts +1 -1
- package/src/core/server.ts +91 -78
- package/src/embedding/embedding-module.ts +52 -0
- package/src/embedding/index.ts +5 -0
- package/src/embedding/providers/ollama-embedding-provider.ts +39 -0
- package/src/embedding/providers/openai-embedding-provider.ts +47 -0
- package/src/embedding/service.ts +55 -0
- package/src/embedding/types.ts +27 -0
- package/src/index.ts +11 -1
- package/src/mcp/decorators.ts +60 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/mcp-module.ts +58 -0
- package/src/mcp/registry.ts +72 -0
- package/src/mcp/server.ts +164 -0
- package/src/mcp/types.ts +63 -0
- package/src/prompt/index.ts +5 -0
- package/src/prompt/prompt-module.ts +61 -0
- package/src/prompt/service.ts +93 -0
- package/src/prompt/stores/file-store.ts +135 -0
- package/src/prompt/stores/memory-store.ts +82 -0
- package/src/prompt/types.ts +84 -0
- package/src/rag/chunkers/markdown-chunker.ts +40 -0
- package/src/rag/chunkers/text-chunker.ts +30 -0
- package/src/rag/decorators.ts +26 -0
- package/src/rag/index.ts +6 -0
- package/src/rag/rag-module.ts +78 -0
- package/src/rag/service.ts +134 -0
- package/src/rag/types.ts +47 -0
- package/src/vector-store/index.ts +5 -0
- package/src/vector-store/stores/memory-store.ts +69 -0
- package/src/vector-store/stores/pinecone-store.ts +123 -0
- package/src/vector-store/stores/qdrant-store.ts +147 -0
- package/src/vector-store/types.ts +77 -0
- package/src/vector-store/vector-store-module.ts +50 -0
- package/tests/ai/ai-module.test.ts +46 -0
- package/tests/ai/ai-service.test.ts +91 -0
- package/tests/ai/tool-registry.test.ts +57 -0
- package/tests/ai-guard/ai-guard-module.test.ts +23 -0
- package/tests/ai-guard/content-moderator.test.ts +65 -0
- package/tests/ai-guard/pii-detector.test.ts +41 -0
- package/tests/conversation/conversation-module.test.ts +26 -0
- package/tests/conversation/conversation-service.test.ts +64 -0
- package/tests/conversation/memory-store.test.ts +68 -0
- package/tests/core/cluster.test.ts +45 -1
- package/tests/embedding/embedding-service.test.ts +55 -0
- package/tests/mcp/mcp-server.test.ts +85 -0
- package/tests/prompt/prompt-module.test.ts +30 -0
- package/tests/prompt/prompt-service.test.ts +74 -0
- package/tests/rag/chunkers.test.ts +58 -0
- package/tests/rag/rag-service.test.ts +66 -0
- package/tests/vector-store/memory-vector-store.test.ts +84 -0
- package/tests/interceptor/perf/interceptor-performance.test.ts +0 -340
- package/tests/perf/optimization.test.ts +0 -182
- package/tests/perf/regression.test.ts +0 -120
package/src/core/cluster.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
69
|
+
* 获取已解析的集群模式
|
|
43
70
|
*/
|
|
44
|
-
public
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
`[Cluster] Starting ${this.workerCount} workers on port ${this.port}`,
|
|
48
|
-
);
|
|
71
|
+
public getMode(): 'reusePort' | 'proxy' {
|
|
72
|
+
return this.mode;
|
|
73
|
+
}
|
|
49
74
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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]
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
|
package/src/core/server.ts
CHANGED
|
@@ -75,92 +75,105 @@ export class BunServer {
|
|
|
75
75
|
this.shutdownPromise = undefined;
|
|
76
76
|
this.shutdownResolve = undefined;
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
} else {
|
|
128
|
+
this.activeRequests--;
|
|
129
|
+
if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
|
|
130
|
+
this.shutdownResolve();
|
|
117
131
|
}
|
|
132
|
+
}
|
|
118
133
|
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
137
|
+
const websocketHandlers = {
|
|
138
|
+
open: async (ws: import("bun").ServerWebSocket<WebSocketConnectionData>) => {
|
|
139
|
+
await this.options.websocketRegistry?.handleOpen(ws);
|
|
147
140
|
},
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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,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
|
+
}
|