@donkeylabs/server 2.0.26 → 2.0.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "2.0.26",
3
+ "version": "2.0.27",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
@@ -34,6 +34,7 @@ export class SqliteJobAdapter implements JobAdapter {
34
34
  this.ensureDir(dbPath);
35
35
 
36
36
  this.db = new Database(dbPath);
37
+ this.db.run("PRAGMA busy_timeout = 5000");
37
38
  this.init();
38
39
 
39
40
  // Start cleanup timer
@@ -80,6 +80,7 @@ export class KyselyLogsAdapter implements LogsAdapter {
80
80
  // Enable WAL mode for better concurrent read/write performance
81
81
  sqliteDb.exec("PRAGMA journal_mode = WAL");
82
82
  sqliteDb.exec("PRAGMA synchronous = NORMAL");
83
+ sqliteDb.exec("PRAGMA busy_timeout = 5000");
83
84
 
84
85
  this.db = new Kysely<Database>({
85
86
  dialect: new SqliteDialect({
@@ -52,6 +52,7 @@ export class SqliteProcessAdapter implements ProcessAdapter {
52
52
  this.ensureDir(dbPath);
53
53
 
54
54
  this.db = new Database(dbPath);
55
+ this.db.run("PRAGMA busy_timeout = 5000");
55
56
  this.init();
56
57
 
57
58
  // Start cleanup timer
@@ -649,8 +649,12 @@ export interface WorkflowsConfig {
649
649
  heartbeatTimeout?: number;
650
650
  /** Timeout waiting for isolated subprocess readiness (ms, default: 10000) */
651
651
  readyTimeout?: number;
652
+ /** Resume strategy for orphaned workflows (default: "blocking") */
653
+ resumeStrategy?: WorkflowResumeStrategy;
652
654
  }
653
655
 
656
+ export type WorkflowResumeStrategy = "blocking" | "background" | "skip";
657
+
654
658
  /** Options for registering a workflow */
655
659
  export interface WorkflowRegisterOptions {
656
660
  /**
@@ -687,7 +691,7 @@ export interface Workflows {
687
691
  /** Get all workflow instances with optional filtering (for admin dashboard) */
688
692
  getAllInstances(options?: GetAllWorkflowsOptions): Promise<WorkflowInstance[]>;
689
693
  /** Resume workflows after server restart */
690
- resume(): Promise<void>;
694
+ resume(options?: { strategy?: WorkflowResumeStrategy }): Promise<void>;
691
695
  /** Stop the workflow service */
692
696
  stop(): Promise<void>;
693
697
  /** Set core services (called after initialization to resolve circular dependency) */
@@ -739,6 +743,7 @@ class WorkflowsImpl implements Workflows {
739
743
  private dbPath?: string;
740
744
  private heartbeatTimeoutMs: number;
741
745
  private readyTimeoutMs: number;
746
+ private resumeStrategy!: WorkflowResumeStrategy;
742
747
  private workflowModulePaths = new Map<string, string>();
743
748
  private isolatedProcesses = new Map<string, IsolatedProcessInfo>();
744
749
  private readyWaiters = new Map<
@@ -772,6 +777,7 @@ class WorkflowsImpl implements Workflows {
772
777
  this.dbPath = config.dbPath;
773
778
  this.heartbeatTimeoutMs = config.heartbeatTimeout ?? 60000;
774
779
  this.readyTimeoutMs = config.readyTimeout ?? 10000;
780
+ this.resumeStrategy = config.resumeStrategy ?? "blocking";
775
781
  }
776
782
 
777
783
  private getSocketServer(): WorkflowSocketServer {
@@ -975,38 +981,64 @@ class WorkflowsImpl implements Workflows {
975
981
  return this.adapter.getAllInstances(options);
976
982
  }
977
983
 
978
- async resume(): Promise<void> {
984
+ async resume(options?: { strategy?: WorkflowResumeStrategy }): Promise<void> {
985
+ const strategy = options?.strategy ?? this.resumeStrategy;
979
986
  const running = await this.adapter.getRunningInstances();
980
987
 
981
- for (const instance of running) {
988
+ if (this.dbPath) {
989
+ await this.getSocketServer().cleanOrphanedSockets(
990
+ new Set(running.map((instance) => instance.id))
991
+ );
992
+ }
993
+
994
+ if (strategy === "skip") {
995
+ await this.markOrphanedAsFailed(running, "Workflow resume skipped");
996
+ return;
997
+ }
998
+
999
+ const resumeInstance = async (instance: WorkflowInstance) => {
982
1000
  const definition = this.definitions.get(instance.workflowName);
983
1001
  if (!definition) {
984
- // Workflow no longer registered, mark as failed
985
1002
  await this.adapter.updateInstance(instance.id, {
986
1003
  status: "failed",
987
1004
  error: "Workflow definition not found after restart",
988
1005
  completedAt: new Date(),
989
1006
  });
990
- continue;
1007
+ return;
991
1008
  }
992
1009
 
993
1010
  console.log(`[Workflows] Resuming workflow instance ${instance.id}`);
994
1011
 
995
- // Check isolation mode and call appropriate method
996
1012
  const isIsolated = definition.isolated !== false;
997
1013
  const modulePath = this.workflowModulePaths.get(instance.workflowName);
998
1014
 
999
1015
  if (isIsolated && modulePath && this.dbPath) {
1000
- try {
1001
- await this.executeIsolatedWorkflow(instance.id, definition, instance.input, modulePath);
1002
- } catch (error) {
1016
+ await this.executeIsolatedWorkflow(instance.id, definition, instance.input, modulePath);
1017
+ } else {
1018
+ this.startInlineWorkflow(instance.id, definition);
1019
+ }
1020
+ };
1021
+
1022
+ if (strategy === "background") {
1023
+ for (const instance of running) {
1024
+ resumeInstance(instance).catch((error) => {
1003
1025
  console.error(
1004
- `[Workflows] Failed to resume isolated workflow ${instance.id}:`,
1026
+ `[Workflows] Failed to resume workflow ${instance.id}:`,
1005
1027
  error instanceof Error ? error.message : String(error)
1006
1028
  );
1007
- }
1008
- } else {
1009
- this.startInlineWorkflow(instance.id, definition);
1029
+ });
1030
+ }
1031
+ return;
1032
+ }
1033
+
1034
+ for (const instance of running) {
1035
+ try {
1036
+ await resumeInstance(instance);
1037
+ } catch (error) {
1038
+ console.error(
1039
+ `[Workflows] Failed to resume workflow ${instance.id}:`,
1040
+ error instanceof Error ? error.message : String(error)
1041
+ );
1010
1042
  }
1011
1043
  }
1012
1044
  }
@@ -1710,6 +1742,34 @@ class WorkflowsImpl implements Workflows {
1710
1742
  this.rejectIsolatedReady(instanceId, new Error("Isolated workflow cleaned up"));
1711
1743
  }
1712
1744
 
1745
+ private async markOrphanedAsFailed(
1746
+ instances: WorkflowInstance[],
1747
+ reason: string
1748
+ ): Promise<void> {
1749
+ for (const instance of instances) {
1750
+ await this.adapter.updateInstance(instance.id, {
1751
+ status: "failed",
1752
+ error: reason,
1753
+ completedAt: new Date(),
1754
+ });
1755
+
1756
+ await this.emitEvent("workflow.failed", {
1757
+ instanceId: instance.id,
1758
+ workflowName: instance.workflowName,
1759
+ error: reason,
1760
+ });
1761
+
1762
+ if (this.sse) {
1763
+ this.sse.broadcast(`workflow:${instance.id}`, "failed", { error: reason });
1764
+ this.sse.broadcast("workflows:all", "workflow.failed", {
1765
+ instanceId: instance.id,
1766
+ workflowName: instance.workflowName,
1767
+ error: reason,
1768
+ });
1769
+ }
1770
+ }
1771
+ }
1772
+
1713
1773
  /**
1714
1774
  * Reset heartbeat timeout for an isolated workflow
1715
1775
  */
package/src/server.ts CHANGED
@@ -81,6 +81,11 @@ export interface ServerConfig {
81
81
  rateLimiter?: RateLimiterConfig;
82
82
  errors?: ErrorsConfig;
83
83
  workflows?: WorkflowsConfig;
84
+ /**
85
+ * Resume strategy for workflows on startup.
86
+ * Defaults to "blocking" for server mode and "background" for adapter mode.
87
+ */
88
+ workflowsResumeStrategy?: "blocking" | "background" | "skip";
84
89
  processes?: ProcessesConfig;
85
90
  audit?: AuditConfig;
86
91
  websocket?: WebSocketConfig;
@@ -214,6 +219,8 @@ export class AppServer {
214
219
  private isInitialized = false;
215
220
  private initializationPromise: Promise<void> | null = null;
216
221
  private generateModeSetup = false;
222
+ private initMode: "adapter" | "server" = "server";
223
+ private workflowsResumeStrategy?: "blocking" | "background" | "skip";
217
224
 
218
225
  // Custom services registry
219
226
  private serviceFactories = new Map<string, ServiceFactory<any>>();
@@ -225,6 +232,7 @@ export class AppServer {
225
232
  const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
226
233
  this.port = options.port ?? envPort ?? 3000;
227
234
  this.maxPortAttempts = options.maxPortAttempts ?? 5;
235
+ this.workflowsResumeStrategy = options.workflowsResumeStrategy ?? options.workflows?.resumeStrategy;
228
236
 
229
237
  // Determine if we should use legacy databases
230
238
  const useLegacy = options.useLegacyCoreDatabases ?? false;
@@ -986,6 +994,7 @@ ${factoryFunction}
986
994
  * Used by adapters (e.g., SvelteKit) that manage their own HTTP server.
987
995
  */
988
996
  async initialize(): Promise<void> {
997
+ this.initMode = "adapter";
989
998
  // Handle CLI type generation mode - exit early before any initialization
990
999
  if (process.env.DONKEYLABS_GENERATE === "1") {
991
1000
  this.outputRoutesForGeneration();
@@ -1038,7 +1047,13 @@ ${factoryFunction}
1038
1047
  this.coreServices.cron.start();
1039
1048
  this.coreServices.jobs.start();
1040
1049
  await this.coreServices.workflows.resolveDbPath();
1041
- await this.coreServices.workflows.resume();
1050
+ const defaultStrategy = this.initMode === "adapter" ? "background" : undefined;
1051
+ const strategy = this.workflowsResumeStrategy ?? defaultStrategy;
1052
+ if (strategy) {
1053
+ await this.coreServices.workflows.resume({ strategy });
1054
+ } else {
1055
+ await this.coreServices.workflows.resume();
1056
+ }
1042
1057
  this.coreServices.processes.start();
1043
1058
  logger.info("Background services started (cron, jobs, workflows, processes)");
1044
1059
 
@@ -1252,6 +1267,7 @@ ${factoryFunction}
1252
1267
  * 5. Start the HTTP server
1253
1268
  */
1254
1269
  async start() {
1270
+ this.initMode = "server";
1255
1271
  // Handle CLI type generation mode - exit early before any initialization
1256
1272
  if (process.env.DONKEYLABS_GENERATE === "1") {
1257
1273
  this.outputRoutesForGeneration();