bm2 1.0.32 → 1.0.34
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 +29 -6
- package/package.json +1 -1
- package/src/daemon.ts +12 -2
- package/src/index.ts +12 -5
- package/src/process-manager.ts +12 -7
package/README.md
CHANGED
|
@@ -451,43 +451,66 @@ bm2 reset all
|
|
|
451
451
|
|
|
452
452
|
Cluster mode spawns multiple instances of your application, each running in its own process. This is ideal for CPU-bound workloads and for taking full advantage of multi-core servers.
|
|
453
453
|
|
|
454
|
-
```
|
|
454
|
+
```bash
|
|
455
455
|
bm2 start server.ts --name api --instances max
|
|
456
|
-
```
|
|
457
456
|
|
|
458
457
|
```
|
|
458
|
+
|
|
459
|
+
```bash
|
|
459
460
|
bm2 start server.ts --name api --instances 4
|
|
460
|
-
```
|
|
461
461
|
|
|
462
462
|
```
|
|
463
|
+
|
|
464
|
+
```bash
|
|
463
465
|
bm2 start server.ts --name api --instances 4 --port 3000
|
|
466
|
+
|
|
464
467
|
```
|
|
465
468
|
|
|
469
|
+
#### ⚠️ Current Status & Limitations
|
|
470
|
+
|
|
471
|
+
While `bm2` provides the orchestration for clustering, please note that **Bun’s native cluster implementation is currently limited by the underlying OS:**
|
|
472
|
+
|
|
473
|
+
* **Linux Only:** Port sharing via `reusePort` is only fully supported on **Linux**.
|
|
474
|
+
* **macOS & Windows:** Due to OS-level limitations with `SO_REUSEPORT`, these platforms ignore the `reusePort` option. On these systems, clustering may result in "Address already in use" errors if attempting to bind multiple workers to the same port.
|
|
475
|
+
|
|
476
|
+
`bm2` leverages the native [Bun.serve cluster logic](https://www.google.com/search?q=https://bun.sh/docs/api/http%23cluster) to ensure maximum performance, but it remains subject to the runtime's maturity.
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
#### Environment Variables
|
|
480
|
+
|
|
466
481
|
Each cluster worker receives the following environment variables:
|
|
467
482
|
|
|
468
483
|
| Variable | Description |
|
|
469
|
-
|
|
484
|
+
| --- | --- |
|
|
470
485
|
| `BM2_CLUSTER` | Set to `"true"` in cluster mode |
|
|
471
486
|
| `BM2_WORKER_ID` | Zero-indexed worker ID |
|
|
472
487
|
| `BM2_INSTANCES` | Total number of instances |
|
|
473
488
|
| `NODE_APP_INSTANCE` | Same as `BM2_WORKER_ID` (PM2 compatibility) |
|
|
474
489
|
| `PORT` | `basePort + workerIndex` (if `--port` is specified) |
|
|
475
490
|
|
|
476
|
-
|
|
491
|
+
---
|
|
477
492
|
|
|
478
|
-
|
|
493
|
+
#### Example: Cluster-Aware Port Binding
|
|
494
|
+
|
|
495
|
+
To enable clustering in Bun, you must explicitly set `reusePort: true`. This allows multiple processes to listen on the same port (on supported OSs).
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
479
498
|
// server.ts
|
|
480
499
|
const workerId = parseInt(process.env.BM2_WORKER_ID || "0");
|
|
481
500
|
const port = parseInt(process.env.PORT || "3000");
|
|
482
501
|
|
|
483
502
|
Bun.serve({
|
|
484
503
|
port,
|
|
504
|
+
// Share the same port across multiple processes
|
|
505
|
+
// This is the important part!
|
|
506
|
+
reusePort: true,
|
|
485
507
|
fetch(req) {
|
|
486
508
|
return new Response(`Hello from worker ${workerId} on port ${port}`);
|
|
487
509
|
},
|
|
488
510
|
});
|
|
489
511
|
|
|
490
512
|
console.log(`Worker ${workerId} listening on :${port}`);
|
|
513
|
+
|
|
491
514
|
```
|
|
492
515
|
|
|
493
516
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bm2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.34",
|
|
4
4
|
"description": "A blazing-fast, full-featured process manager built entirely on Bun native APIs. The modern PM2 replacement — zero Node.js dependencies, pure Bun performance.",
|
|
5
5
|
"main": "src/api.ts",
|
|
6
6
|
"module": "src/api.ts",
|
package/src/daemon.ts
CHANGED
|
@@ -35,6 +35,11 @@ const pm = new ProcessManager();
|
|
|
35
35
|
const dashboard = new Dashboard(pm);
|
|
36
36
|
const moduleManager = new ModuleManager(pm);
|
|
37
37
|
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
|
|
40
|
+
// Checks if '--debug' exists anywhere in the arguments
|
|
41
|
+
const debugMode = args.includes('--debug');
|
|
42
|
+
|
|
38
43
|
// Clean up existing socket
|
|
39
44
|
if (existsSync(DAEMON_SOCKET)) {
|
|
40
45
|
try { unlinkSync(DAEMON_SOCKET); } catch {}
|
|
@@ -223,8 +228,13 @@ async function handleMessage(msg: DaemonMessage): Promise<DaemonResponse> {
|
|
|
223
228
|
default:
|
|
224
229
|
return { type: "error", error: `Unknown command: ${msg.type}`, success: false, id: msg.id };
|
|
225
230
|
}
|
|
226
|
-
} catch (err: any) {
|
|
227
|
-
|
|
231
|
+
} catch (err: Error | any) {
|
|
232
|
+
let error = err.message;
|
|
233
|
+
if (debugMode) {
|
|
234
|
+
error = `Message: ${err.message}\nStack: ${err.stack}`
|
|
235
|
+
console.error(err, err.stack)
|
|
236
|
+
}
|
|
237
|
+
return { type: "error", error, success: false, id: msg.id };
|
|
228
238
|
}
|
|
229
239
|
}
|
|
230
240
|
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import type {
|
|
|
41
41
|
} from "./types";
|
|
42
42
|
import { statusColor } from "./colors";
|
|
43
43
|
import { liveWatchProcess, printProcessTable } from "./process-table";
|
|
44
|
+
import { exists } from "fs/promises";
|
|
44
45
|
|
|
45
46
|
// ---------------------------------------------------------------------------
|
|
46
47
|
// Ensure directory structure exists
|
|
@@ -143,8 +144,10 @@ async function sendToDaemon(msg: DaemonMessage): Promise<DaemonResponse> {
|
|
|
143
144
|
async function loadEcosystemConfig(filePath: string): Promise<EcosystemConfig> {
|
|
144
145
|
|
|
145
146
|
const abs = resolve(filePath);
|
|
147
|
+
|
|
148
|
+
const file = Bun.file(abs);
|
|
146
149
|
|
|
147
|
-
if (!
|
|
150
|
+
if (!(await file.exists())) {
|
|
148
151
|
throw new Error(`Ecosystem file not found: ${abs}`);
|
|
149
152
|
}
|
|
150
153
|
|
|
@@ -153,7 +156,7 @@ async function loadEcosystemConfig(filePath: string): Promise<EcosystemConfig> {
|
|
|
153
156
|
let config;
|
|
154
157
|
|
|
155
158
|
if (ext === ".json") {
|
|
156
|
-
config = (await
|
|
159
|
+
config = (await file.json()) as EcosystemConfig;
|
|
157
160
|
} else {
|
|
158
161
|
// .ts, .js, .mjs — dynamic import
|
|
159
162
|
const mod = await import(abs);
|
|
@@ -362,8 +365,11 @@ async function cmdStart(args: string[]) {
|
|
|
362
365
|
opts.script = resolve(scriptOrConfig);
|
|
363
366
|
|
|
364
367
|
const cwd = path.dirname(opts.script);
|
|
365
|
-
|
|
366
|
-
|
|
368
|
+
|
|
369
|
+
opts.cwd = cwd;
|
|
370
|
+
|
|
371
|
+
const res = await sendToDaemon({ type: "start", data: opts });
|
|
372
|
+
|
|
367
373
|
if (!res.success) {
|
|
368
374
|
console.error(colorize(`Error: ${res.error}`, "red"));
|
|
369
375
|
process.exit(1);
|
|
@@ -697,7 +703,8 @@ async function cmdDeploy(args: string[]) {
|
|
|
697
703
|
process.exit(1);
|
|
698
704
|
}
|
|
699
705
|
|
|
700
|
-
const
|
|
706
|
+
const config = await loadEcosystemConfig(configFile);
|
|
707
|
+
|
|
701
708
|
if (!config.deploy || !config.deploy[environment]) {
|
|
702
709
|
console.error(colorize(`Deploy environment "${environment}" not found in config`, "red"));
|
|
703
710
|
process.exit(1);
|
package/src/process-manager.ts
CHANGED
|
@@ -65,12 +65,13 @@ import path from "path";
|
|
|
65
65
|
|
|
66
66
|
options.script = path.isAbsolute(options.script)
|
|
67
67
|
? options.script
|
|
68
|
-
: path.
|
|
69
|
-
|
|
68
|
+
: path.join(options.cwd!, options.script);
|
|
69
|
+
|
|
70
|
+
|
|
70
71
|
if (!(await Bun.file(options.script).exists())) {
|
|
71
72
|
throw new Error(`Script not found: ${options.script}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
73
|
+
}
|
|
74
|
+
|
|
74
75
|
if (isCluster) {
|
|
75
76
|
// In cluster mode, each instance is a separate container
|
|
76
77
|
for (let i = 0; i < resolvedInstances; i++) {
|
|
@@ -94,6 +95,7 @@ import path from "path";
|
|
|
94
95
|
await container.start();
|
|
95
96
|
states.push(container.getState());
|
|
96
97
|
}
|
|
98
|
+
|
|
97
99
|
} else {
|
|
98
100
|
const id = this.nextId++;
|
|
99
101
|
const name =
|
|
@@ -103,8 +105,11 @@ import path from "path";
|
|
|
103
105
|
|
|
104
106
|
const config = this.buildConfig(id, name, options, 1, 0);
|
|
105
107
|
const container = new ProcessContainer(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
id, config,
|
|
109
|
+
this.logManager,
|
|
110
|
+
this.clusterManager,
|
|
111
|
+
this.healthChecker,
|
|
112
|
+
this.cronManager
|
|
108
113
|
);
|
|
109
114
|
|
|
110
115
|
this.processes.set(id, container);
|
|
@@ -113,7 +118,7 @@ import path from "path";
|
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
return states;
|
|
116
|
-
|
|
121
|
+
}
|
|
117
122
|
|
|
118
123
|
private buildConfig(
|
|
119
124
|
id: number,
|