@donkeylabs/server 2.0.28 → 2.0.30

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/src/server.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
- import { dirname } from "node:path";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { PluginManager, type CoreServices, type ConfiguredPlugin } from "./core";
5
6
  import { type IRouter, type RouteDefinition, type ServerContext, type HandlerRegistry } from "./router";
6
7
  import { Handlers } from "./handlers";
@@ -87,6 +88,13 @@ export interface ServerConfig {
87
88
  */
88
89
  workflowsResumeStrategy?: "blocking" | "background" | "skip";
89
90
  processes?: ProcessesConfig;
91
+ /** Watchdog subprocess configuration */
92
+ watchdog?: {
93
+ enabled?: boolean;
94
+ intervalMs?: number;
95
+ services?: ("workflows" | "jobs" | "processes")[];
96
+ killGraceMs?: number;
97
+ };
90
98
  audit?: AuditConfig;
91
99
  websocket?: WebSocketConfig;
92
100
  storage?: StorageConfig;
@@ -221,6 +229,9 @@ export class AppServer {
221
229
  private generateModeSetup = false;
222
230
  private initMode: "adapter" | "server" = "server";
223
231
  private workflowsResumeStrategy?: "blocking" | "background" | "skip";
232
+ private watchdogConfig?: ServerConfig["watchdog"];
233
+ private watchdogStarted = false;
234
+ private options: ServerConfig;
224
235
 
225
236
  // Custom services registry
226
237
  private serviceFactories = new Map<string, ServiceFactory<any>>();
@@ -228,11 +239,13 @@ export class AppServer {
228
239
  private generateModeTimer?: ReturnType<typeof setTimeout>;
229
240
 
230
241
  constructor(options: ServerConfig) {
242
+ this.options = options;
231
243
  // Port priority: explicit config > PORT env var > default 3000
232
244
  const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
233
245
  this.port = options.port ?? envPort ?? 3000;
234
246
  this.maxPortAttempts = options.maxPortAttempts ?? 5;
235
247
  this.workflowsResumeStrategy = options.workflowsResumeStrategy ?? options.workflows?.resumeStrategy;
248
+ this.watchdogConfig = options.watchdog;
236
249
 
237
250
  // Determine if we should use legacy databases
238
251
  const useLegacy = options.useLegacyCoreDatabases ?? false;
@@ -286,6 +299,10 @@ export class AppServer {
286
299
  events,
287
300
  logger,
288
301
  adapter: jobAdapter,
302
+ external: {
303
+ ...options.jobs?.external,
304
+ useWatchdog: options.watchdog?.enabled ? true : options.jobs?.external?.useWatchdog,
305
+ },
289
306
  // Disable built-in persistence when using Kysely adapter
290
307
  persist: useLegacy ? options.jobs?.persist : false,
291
308
  });
@@ -297,12 +314,14 @@ export class AppServer {
297
314
  jobs,
298
315
  sse,
299
316
  adapter: workflowAdapter,
317
+ useWatchdog: options.watchdog?.enabled ? true : options.workflows?.useWatchdog,
300
318
  });
301
319
 
302
320
  // Processes - still uses its own adapter pattern but can use Kysely
303
321
  const processes = createProcesses({
304
322
  ...options.processes,
305
323
  events,
324
+ useWatchdog: options.watchdog?.enabled ? true : options.processes?.useWatchdog,
306
325
  });
307
326
 
308
327
  // New services
@@ -1055,6 +1074,7 @@ ${factoryFunction}
1055
1074
  await this.coreServices.workflows.resume();
1056
1075
  }
1057
1076
  this.coreServices.processes.start();
1077
+ await this.startWatchdog();
1058
1078
  logger.info("Background services started (cron, jobs, workflows, processes)");
1059
1079
 
1060
1080
  for (const router of this.routers) {
@@ -1073,6 +1093,69 @@ ${factoryFunction}
1073
1093
  await this.runReadyHandlers();
1074
1094
  }
1075
1095
 
1096
+ private async startWatchdog(): Promise<void> {
1097
+ if (!this.watchdogConfig?.enabled) return;
1098
+ if (this.watchdogStarted) return;
1099
+
1100
+ const executorPath = join(dirname(fileURLToPath(import.meta.url)), "core", "watchdog-executor.ts");
1101
+ const services = this.watchdogConfig.services ?? ["workflows", "jobs", "processes"];
1102
+ const workflowsDbPath = this.coreServices.workflows.getDbPath?.();
1103
+ const jobsDbPath = (this.options.jobs?.dbPath ?? workflowsDbPath ?? ".donkeylabs/jobs.db") as string;
1104
+ const processesDbPath = (this.options.processes?.adapter?.path ?? ".donkeylabs/processes.db") as string;
1105
+
1106
+ const config = {
1107
+ intervalMs: this.watchdogConfig.intervalMs ?? 5000,
1108
+ services,
1109
+ killGraceMs: this.watchdogConfig.killGraceMs ?? 5000,
1110
+ workflowHeartbeatTimeoutMs: this.options.workflows?.heartbeatTimeout ?? 60000,
1111
+ jobDefaults: {
1112
+ heartbeatTimeoutMs: this.options.jobs?.external?.defaultHeartbeatTimeout ?? 30000,
1113
+ killGraceMs: this.options.jobs?.external?.killGraceMs ?? this.watchdogConfig.killGraceMs ?? 5000,
1114
+ },
1115
+ jobConfigs: this.coreServices.jobs.getExternalJobConfigs(),
1116
+ workflows: workflowsDbPath ? { dbPath: workflowsDbPath } : undefined,
1117
+ jobs: jobsDbPath ? { dbPath: jobsDbPath } : undefined,
1118
+ processes: processesDbPath ? { dbPath: processesDbPath } : undefined,
1119
+ sqlitePragmas: this.options.workflows?.sqlitePragmas,
1120
+ };
1121
+
1122
+ try {
1123
+ this.coreServices.processes.register({
1124
+ name: "__watchdog",
1125
+ config: {
1126
+ command: "bun",
1127
+ args: ["run", executorPath],
1128
+ env: {
1129
+ DONKEYLABS_WATCHDOG_CONFIG: JSON.stringify(config),
1130
+ },
1131
+ heartbeat: { intervalMs: 5000, timeoutMs: 30000 },
1132
+ },
1133
+ });
1134
+ } catch {
1135
+ // Already registered
1136
+ }
1137
+
1138
+ await this.coreServices.processes.spawn("__watchdog", {
1139
+ metadata: { role: "watchdog" },
1140
+ });
1141
+
1142
+ this.coreServices.events.on("process.event", async (data: any) => {
1143
+ if (data?.name !== "__watchdog") return;
1144
+ if (!data.event) return;
1145
+
1146
+ await this.coreServices.events.emit(data.event, data.data ?? {});
1147
+
1148
+ if (data.event.startsWith("workflow.watchdog")) {
1149
+ const instanceId = data.data?.instanceId;
1150
+ if (instanceId && this.coreServices.sse) {
1151
+ this.coreServices.sse.broadcast(`workflow:${instanceId}`, data.event, data.data ?? {});
1152
+ }
1153
+ }
1154
+ });
1155
+
1156
+ this.watchdogStarted = true;
1157
+ }
1158
+
1076
1159
  /**
1077
1160
  * Handle a single API request. Used by adapters.
1078
1161
  * Returns null if the route is not found.