@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 +53 -2
- package/package.json +1 -1
- package/src/core/processes.ts +55 -1
- package/src/core.ts +20 -1
- package/src/index.ts +2 -0
- package/src/server.ts +24 -1
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
package/src/core/processes.ts
CHANGED
|
@@ -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
|
-
|
|
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
package/src/server.ts
CHANGED
|
@@ -855,7 +855,30 @@ export class AppServer {
|
|
|
855
855
|
}
|
|
856
856
|
}
|
|
857
857
|
|
|
858
|
-
|
|
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
|
/**
|