@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
@@ -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
- // Server can send messages to the process (e.g., "stop", "config update")
225
- // For now, just log them
226
- console.log(`[ProcessClient] Received from server:`, message);
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;