@donkeylabs/server 2.2.0 → 2.3.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/docs/processes.md +92 -0
- package/package.json +1 -1
- package/src/core/process-client.ts +37 -4
package/docs/processes.md
CHANGED
|
@@ -269,6 +269,11 @@ client.connected; // true | false
|
|
|
269
269
|
// Emit a typed event to the server
|
|
270
270
|
await client.emit("progress", { percent: 50, fps: 30 });
|
|
271
271
|
|
|
272
|
+
// Register a handler for server-sent messages (alternative to onMessage in connect options)
|
|
273
|
+
client.onMessage((message) => {
|
|
274
|
+
console.log("Received from server:", message);
|
|
275
|
+
});
|
|
276
|
+
|
|
272
277
|
// Disconnect when done
|
|
273
278
|
client.disconnect();
|
|
274
279
|
```
|
|
@@ -358,6 +363,9 @@ interface Processes {
|
|
|
358
363
|
/** Get processes by name */
|
|
359
364
|
getByName(name: string): ManagedProcess[];
|
|
360
365
|
|
|
366
|
+
/** Send a message to a running process */
|
|
367
|
+
send(processId: string, message: any): Promise<boolean>;
|
|
368
|
+
|
|
361
369
|
/** Stop a process */
|
|
362
370
|
stop(processId: string, signal?: NodeJS.Signals): Promise<void>;
|
|
363
371
|
|
|
@@ -563,6 +571,89 @@ ctx.core.events.on("process.stats", ({ processId, name, stats }) => {
|
|
|
563
571
|
});
|
|
564
572
|
```
|
|
565
573
|
|
|
574
|
+
## Server-to-Process Communication
|
|
575
|
+
|
|
576
|
+
The server can send messages to running processes via `ctx.core.processes.send()`. The ProcessClient receives these messages through the `onMessage` callback.
|
|
577
|
+
|
|
578
|
+
### Sending Messages from Server
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
// In a route handler or service
|
|
582
|
+
await ctx.core.processes.send(processId, {
|
|
583
|
+
type: "subscribe",
|
|
584
|
+
channel: "live-scores",
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
await ctx.core.processes.send(processId, {
|
|
588
|
+
type: "config_update",
|
|
589
|
+
settings: { maxConnections: 100 },
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Receiving Messages in Worker
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// Option 1: In connect options
|
|
597
|
+
const client = await ProcessClient.connect({
|
|
598
|
+
onMessage: (message) => {
|
|
599
|
+
switch (message.type) {
|
|
600
|
+
case "subscribe":
|
|
601
|
+
subscribeToChannel(message.channel);
|
|
602
|
+
break;
|
|
603
|
+
case "config_update":
|
|
604
|
+
applyConfig(message.settings);
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Option 2: Register handler after connecting
|
|
611
|
+
const client = await ProcessClient.connect();
|
|
612
|
+
client.onMessage((message) => {
|
|
613
|
+
console.log("Received:", message);
|
|
614
|
+
});
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### Example: WebSocket Daemon with Server Commands
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
// Server: define and spawn the WebSocket daemon
|
|
621
|
+
server.getCore().processes.define("ws-daemon", {
|
|
622
|
+
command: "bun",
|
|
623
|
+
args: ["./workers/ws-daemon.ts"],
|
|
624
|
+
events: {
|
|
625
|
+
ready: z.object({ port: z.number() }),
|
|
626
|
+
clientCount: z.object({ count: z.number() }),
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const processId = await ctx.core.processes.spawn("ws-daemon", {
|
|
631
|
+
metadata: { port: 8080 },
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// Server: send commands to the daemon
|
|
635
|
+
await ctx.core.processes.send(processId, {
|
|
636
|
+
type: "broadcast",
|
|
637
|
+
message: "Server maintenance in 5 minutes",
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Worker: ws-daemon.ts
|
|
641
|
+
import { ProcessClient } from "@donkeylabs/server/process-client";
|
|
642
|
+
|
|
643
|
+
const client = await ProcessClient.connect({
|
|
644
|
+
onMessage: (message) => {
|
|
645
|
+
if (message.type === "broadcast") {
|
|
646
|
+
// Broadcast to all connected WebSocket clients
|
|
647
|
+
for (const ws of connections) {
|
|
648
|
+
ws.send(JSON.stringify({ type: "announcement", text: message.message }));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
client.emit("ready", { port: client.metadata.port });
|
|
655
|
+
```
|
|
656
|
+
|
|
566
657
|
## Heartbeat Monitoring
|
|
567
658
|
|
|
568
659
|
The ProcessClient automatically sends heartbeats. If heartbeats stop:
|
|
@@ -651,3 +742,4 @@ process.on("SIGTERM", () => client.disconnect());
|
|
|
651
742
|
3. **Use typed events** - Define event schemas for type safety
|
|
652
743
|
4. **Monitor heartbeats** - Set appropriate timeout for your use case
|
|
653
744
|
5. **Keep wrappers thin** - Business logic should be in the actual process
|
|
745
|
+
6. **Use onMessage for commands** - Register `onMessage` to receive server commands for stateful workers
|
package/package.json
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* ```ts
|
|
9
9
|
* import { ProcessClient } from "@donkeylabs/server/process-client";
|
|
10
10
|
*
|
|
11
|
-
* const client = await ProcessClient.connect(
|
|
11
|
+
* const client = await ProcessClient.connect({
|
|
12
|
+
* onMessage: (message) => {
|
|
13
|
+
* if (message.type === "subscribe") { ... }
|
|
14
|
+
* },
|
|
15
|
+
* });
|
|
12
16
|
*
|
|
13
17
|
* // Access metadata passed during spawn
|
|
14
18
|
* const { inputPath, outputPath } = client.metadata;
|
|
@@ -83,6 +87,8 @@ export interface ProcessClientConfig {
|
|
|
83
87
|
maxReconnectAttempts?: number;
|
|
84
88
|
/** Stats emission configuration */
|
|
85
89
|
stats?: StatsConfig;
|
|
90
|
+
/** Callback for messages sent from the server via ctx.core.processes.send() */
|
|
91
|
+
onMessage?: (message: any) => void | Promise<void>;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
export interface ProcessClient {
|
|
@@ -94,6 +100,8 @@ export interface ProcessClient {
|
|
|
94
100
|
readonly connected: boolean;
|
|
95
101
|
/** Emit a typed event to the server */
|
|
96
102
|
emit(event: string, data?: Record<string, any>): Promise<boolean>;
|
|
103
|
+
/** Register a handler for messages sent from the server via ctx.core.processes.send() */
|
|
104
|
+
onMessage(handler: (message: any) => void | Promise<void>): void;
|
|
97
105
|
/** Disconnect from the server */
|
|
98
106
|
disconnect(): void;
|
|
99
107
|
}
|
|
@@ -120,6 +128,7 @@ class ProcessClientImpl implements ProcessClient {
|
|
|
120
128
|
private reconnectAttempts = 0;
|
|
121
129
|
private isDisconnecting = false;
|
|
122
130
|
private _connected = false;
|
|
131
|
+
private messageHandler?: (message: any) => void | Promise<void>;
|
|
123
132
|
|
|
124
133
|
// For CPU percentage calculation
|
|
125
134
|
private lastCpuUsage?: NodeJS.CpuUsage;
|
|
@@ -134,6 +143,7 @@ class ProcessClientImpl implements ProcessClient {
|
|
|
134
143
|
this.reconnectInterval = config.reconnectInterval ?? 2000;
|
|
135
144
|
this.maxReconnectAttempts = config.maxReconnectAttempts ?? 30;
|
|
136
145
|
this.statsConfig = config.stats ?? { enabled: false };
|
|
146
|
+
if (config.onMessage) this.messageHandler = config.onMessage;
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
get connected(): boolean {
|
|
@@ -221,9 +231,22 @@ class ProcessClientImpl implements ProcessClient {
|
|
|
221
231
|
}
|
|
222
232
|
|
|
223
233
|
private handleServerMessage(message: any): void {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
234
|
+
if (this.messageHandler) {
|
|
235
|
+
try {
|
|
236
|
+
const result = this.messageHandler(message);
|
|
237
|
+
if (result instanceof Promise) {
|
|
238
|
+
result.catch((err) => {
|
|
239
|
+
console.error(`[ProcessClient] Error in onMessage handler:`, err);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error(`[ProcessClient] Error in onMessage handler:`, err);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
onMessage(handler: (message: any) => void | Promise<void>): void {
|
|
249
|
+
this.messageHandler = handler;
|
|
227
250
|
}
|
|
228
251
|
|
|
229
252
|
private scheduleReconnect(): void {
|
|
@@ -412,6 +435,14 @@ export function createProcessClient(config: ProcessClientConfig): ProcessClient
|
|
|
412
435
|
* const client = await ProcessClient.connect({
|
|
413
436
|
* stats: { enabled: true, interval: 2000 }
|
|
414
437
|
* });
|
|
438
|
+
*
|
|
439
|
+
* // With server message handling
|
|
440
|
+
* const client = await ProcessClient.connect({
|
|
441
|
+
* onMessage: (message) => {
|
|
442
|
+
* if (message.type === "subscribe") { ... }
|
|
443
|
+
* if (message.type === "config_update") { ... }
|
|
444
|
+
* },
|
|
445
|
+
* });
|
|
415
446
|
* ```
|
|
416
447
|
*/
|
|
417
448
|
export async function connect(options?: {
|
|
@@ -420,6 +451,8 @@ export async function connect(options?: {
|
|
|
420
451
|
maxReconnectAttempts?: number;
|
|
421
452
|
/** Enable real-time CPU/memory stats emission */
|
|
422
453
|
stats?: StatsConfig;
|
|
454
|
+
/** Callback for messages sent from the server via ctx.core.processes.send() */
|
|
455
|
+
onMessage?: (message: any) => void | Promise<void>;
|
|
423
456
|
}): Promise<ProcessClient> {
|
|
424
457
|
const processId = process.env.DONKEYLABS_PROCESS_ID;
|
|
425
458
|
const socketPath = process.env.DONKEYLABS_SOCKET_PATH;
|