@donkeylabs/server 2.3.0 → 2.4.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
@@ -323,9 +323,12 @@ interface ProcessDefinition {
323
323
  /** Environment variables */
324
324
  env?: Record<string, string>;
325
325
 
326
- /** Typed events the process can emit */
326
+ /** Typed events the process can emit (validated at runtime) */
327
327
  events?: Record<string, ZodSchema>;
328
328
 
329
+ /** Typed command schemas for messages sent TO the process via send() (validated at runtime) */
330
+ commands?: Record<string, ZodSchema>;
331
+
329
332
  /** Heartbeat timeout in ms (default: 30000) */
330
333
  heartbeatTimeout?: number;
331
334
 
@@ -363,9 +366,12 @@ interface Processes {
363
366
  /** Get processes by name */
364
367
  getByName(name: string): ManagedProcess[];
365
368
 
366
- /** Send a message to a running process */
369
+ /** Send a message to a running process (validated against command schemas if defined) */
367
370
  send(processId: string, message: any): Promise<boolean>;
368
371
 
372
+ /** Get all registered process definitions */
373
+ getDefinitions(): Map<string, ProcessDefinition>;
374
+
369
375
  /** Stop a process */
370
376
  stop(processId: string, signal?: NodeJS.Signals): Promise<void>;
371
377
 
@@ -575,6 +581,51 @@ ctx.core.events.on("process.stats", ({ processId, name, stats }) => {
575
581
 
576
582
  The server can send messages to running processes via `ctx.core.processes.send()`. The ProcessClient receives these messages through the `onMessage` callback.
577
583
 
584
+ ### Typed Commands
585
+
586
+ Define command schemas to get runtime validation on `send()`. If a command fails validation, `send()` returns `false` and the message is not sent.
587
+
588
+ ```typescript
589
+ import { z } from "zod";
590
+
591
+ ctx.core.processes.register({
592
+ name: "ws-daemon",
593
+ config: {
594
+ command: "bun",
595
+ args: ["./workers/ws-daemon.ts"],
596
+ },
597
+ events: {
598
+ ready: z.object({ port: z.number() }),
599
+ clientCount: z.object({ count: z.number() }),
600
+ },
601
+ // Commands are validated before sending to the process
602
+ commands: {
603
+ subscribe: z.object({
604
+ type: z.literal("subscribe"),
605
+ channel: z.string(),
606
+ }),
607
+ configUpdate: z.object({
608
+ type: z.literal("configUpdate"),
609
+ settings: z.record(z.any()),
610
+ }),
611
+ },
612
+ });
613
+
614
+ // Valid command - sends successfully
615
+ await ctx.core.processes.send(processId, {
616
+ type: "subscribe",
617
+ channel: "live-scores",
618
+ }); // true
619
+
620
+ // Invalid command - returns false, message not sent
621
+ await ctx.core.processes.send(processId, {
622
+ type: "subscribe",
623
+ channel: 12345, // schema requires string
624
+ }); // false
625
+ ```
626
+
627
+ Commands without schemas are not validated (passthrough). The `type` field of the message is used to match against the command schema name.
628
+
578
629
  ### Sending Messages from Server
579
630
 
580
631
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "2.3.0",
3
+ "version": "2.4.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",
@@ -136,6 +136,19 @@ export interface ProcessDefinition {
136
136
  * ```
137
137
  */
138
138
  events?: Record<string, import("zod").ZodType<any>>;
139
+ /**
140
+ * Command schemas for messages sent TO the process via send().
141
+ * Commands are validated at runtime using safeParse() before sending.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * commands: {
146
+ * subscribe: z.object({ channel: z.string() }),
147
+ * configUpdate: z.object({ settings: z.record(z.any()) }),
148
+ * }
149
+ * ```
150
+ */
151
+ commands?: Record<string, import("zod").ZodType<any>>;
139
152
  /** Called when a message is received from the process */
140
153
  onMessage?: (process: ManagedProcess, message: any) => void | Promise<void>;
141
154
  /** Called when the process crashes unexpectedly */
@@ -209,6 +222,8 @@ export interface Processes {
209
222
  getRunning(): Promise<ManagedProcess[]>;
210
223
  /** Send a message to a process via socket */
211
224
  send(processId: string, message: any): Promise<boolean>;
225
+ /** Get all registered process definitions */
226
+ getDefinitions(): Map<string, ProcessDefinition>;
212
227
  /** Start the service (recovery, monitoring) */
213
228
  start(): void;
214
229
  /** Shutdown the service and all managed processes */
@@ -552,9 +567,32 @@ export class ProcessesImpl implements Processes {
552
567
  }
553
568
 
554
569
  async send(processId: string, message: any): Promise<boolean> {
570
+ // Validate message against command schemas if defined
571
+ if (message && typeof message === "object" && message.type) {
572
+ const proc = await this.adapter.get(processId);
573
+ if (proc) {
574
+ const definition = this.definitions.get(proc.name);
575
+ if (definition?.commands) {
576
+ const commandSchema = definition.commands[message.type];
577
+ if (commandSchema) {
578
+ const result = commandSchema.safeParse(message);
579
+ if (!result.success) {
580
+ console.warn(
581
+ `[Processes] Command validation failed for '${message.type}' on ${proc.name}: ${result.error.message}`
582
+ );
583
+ return false;
584
+ }
585
+ }
586
+ }
587
+ }
588
+ }
555
589
  return this.socketServer.send(processId, message);
556
590
  }
557
591
 
592
+ getDefinitions(): Map<string, ProcessDefinition> {
593
+ return this.definitions;
594
+ }
595
+
558
596
  start(): void {
559
597
  // Recover orphaned processes
560
598
  if (this.autoRecoverOrphans) {
@@ -684,7 +722,23 @@ export class ProcessesImpl implements Processes {
684
722
  // Handle typed event messages from ProcessClient.emit()
685
723
  if (type === "event" && message.event) {
686
724
  const eventName = message.event as string;
687
- const eventData = message.data ?? {};
725
+ let eventData = message.data ?? {};
726
+
727
+ // Validate event data against schema if defined
728
+ const definition = this.definitions.get(proc.name);
729
+ if (definition?.events) {
730
+ const eventSchema = definition.events[eventName];
731
+ if (eventSchema) {
732
+ const result = eventSchema.safeParse(eventData);
733
+ if (!result.success) {
734
+ console.warn(
735
+ `[Processes] Event validation failed for '${eventName}' on ${proc.name}: ${result.error.message}`
736
+ );
737
+ return; // Drop invalid events
738
+ }
739
+ eventData = result.data;
740
+ }
741
+ }
688
742
 
689
743
  // Emit to events service as "process.<name>.<event>"
690
744
  await this.emitEvent(`process.${proc.name}.${eventName}`, {
package/src/core.ts CHANGED
@@ -73,6 +73,25 @@ export type EventSchemas = Record<string, z.ZodType<any>>;
73
73
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
74
74
  export interface EventRegistry {}
75
75
 
76
+ /**
77
+ * Registry for process type definitions.
78
+ * Augment this interface to add typed process commands/events:
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * declare module "@donkeylabs/server" {
83
+ * interface ProcessRegistry {
84
+ * "video-encoder": {
85
+ * events: { progress: { percent: number }; complete: { outputPath: string } };
86
+ * commands: { subscribe: { channel: string }; configUpdate: { settings: Record<string, any> } };
87
+ * };
88
+ * }
89
+ * }
90
+ * ```
91
+ */
92
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
93
+ export interface ProcessRegistry {}
94
+
76
95
  /**
77
96
  * Define server-level events with Zod schemas.
78
97
  * Events defined here will be typed and available across the app.
@@ -240,7 +259,7 @@ type ExtractServices<T extends readonly (keyof PluginRegistry)[] | undefined> =
240
259
  T extends readonly []
241
260
  ? {}
242
261
  : T extends readonly (infer K)[]
243
- ? K extends keyof PluginRegistry
262
+ ? [K] extends [keyof PluginRegistry]
244
263
  ? { [P in K]: PluginRegistry[P] extends { service: infer S } ? S : unknown }
245
264
  : {}
246
265
  : {};
package/src/index.ts CHANGED
@@ -51,7 +51,9 @@ export {
51
51
  defineEvents,
52
52
  type EventRegistry,
53
53
  type EventSchemas,
54
+ // Registries
54
55
  type PluginRegistry,
56
+ type ProcessRegistry,
55
57
  type PluginHandlerRegistry,
56
58
  type PluginMiddlewareRegistry,
57
59
  type CoreServices,
package/src/server.ts CHANGED
@@ -855,7 +855,30 @@ export class AppServer {
855
855
  }
856
856
  }
857
857
 
858
- console.log(JSON.stringify({ routes }));
858
+ // Collect process definitions with their schemas
859
+ const processes = [];
860
+ const definitions = this.coreServices.processes.getDefinitions();
861
+ for (const [name, def] of definitions) {
862
+ const events: Record<string, string> = {};
863
+ if (def.events) {
864
+ for (const [eventName, schema] of Object.entries(def.events)) {
865
+ events[eventName] = zodSchemaToTs(schema);
866
+ }
867
+ }
868
+ const commands: Record<string, string> = {};
869
+ if (def.commands) {
870
+ for (const [cmdName, schema] of Object.entries(def.commands)) {
871
+ commands[cmdName] = zodSchemaToTs(schema);
872
+ }
873
+ }
874
+ processes.push({
875
+ name,
876
+ events: Object.keys(events).length > 0 ? events : undefined,
877
+ commands: Object.keys(commands).length > 0 ? commands : undefined,
878
+ });
879
+ }
880
+
881
+ console.log(JSON.stringify({ routes, processes }));
859
882
  }
860
883
 
861
884
  /**