@donkeylabs/server 2.0.32 → 2.0.34

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/workflows.md CHANGED
@@ -98,14 +98,10 @@ const server = new AppServer({
98
98
  db,
99
99
  workflows: {
100
100
  concurrentWorkflows: 1, // default for all workflows (0 = unlimited)
101
- concurrentWorkflowsByName: {
102
- testWorkflow: 1,
103
- ingestionWorkflow: 1,
104
- },
105
101
  },
106
102
  });
107
103
 
108
- // Or per-register override
104
+ // Per-register override
109
105
  ctx.core.workflows.register(orderWorkflow, { maxConcurrent: 1 });
110
106
  ```
111
107
 
@@ -338,6 +334,10 @@ You can tune subprocess termination and SQLite pragmas used by isolated workflow
338
334
  ```ts
339
335
  const server = new AppServer({
340
336
  db,
337
+ database: {
338
+ type: "postgres",
339
+ connectionString: process.env.DATABASE_URL!,
340
+ },
341
341
  watchdog: {
342
342
  enabled: true,
343
343
  intervalMs: 5000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "2.0.32",
3
+ "version": "2.0.34",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
package/src/core/index.ts CHANGED
@@ -135,6 +135,7 @@ export {
135
135
  type Workflows,
136
136
  type WorkflowsConfig,
137
137
  type SqlitePragmaConfig,
138
+ type WorkflowDatabaseConfig,
138
139
  type WorkflowRegisterOptions,
139
140
  type WorkflowDefinition,
140
141
  type WorkflowInstance,
@@ -1,4 +1,4 @@
1
- import { Kysely } from "kysely";
1
+ import { Kysely, PostgresDialect, MysqlDialect } from "kysely";
2
2
  import { BunSqliteDialect } from "kysely-bun-sqlite";
3
3
  import Database from "bun:sqlite";
4
4
  import {
@@ -30,13 +30,17 @@ export interface SubprocessPluginMetadata {
30
30
  }
31
31
 
32
32
  export interface SubprocessBootstrapOptions {
33
- dbPath: string;
33
+ dbPath?: string;
34
34
  coreConfig?: Record<string, any>;
35
35
  sqlitePragmas?: {
36
36
  busyTimeout?: number;
37
37
  synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA";
38
38
  journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
39
39
  };
40
+ database?: {
41
+ type: "sqlite" | "postgres" | "mysql";
42
+ connectionString: string;
43
+ };
40
44
  pluginMetadata: SubprocessPluginMetadata;
41
45
  startServices?: {
42
46
  cron?: boolean;
@@ -57,20 +61,7 @@ export interface SubprocessBootstrapResult {
57
61
  export async function bootstrapSubprocess(
58
62
  options: SubprocessBootstrapOptions
59
63
  ): Promise<SubprocessBootstrapResult> {
60
- const sqlite = new Database(options.dbPath);
61
- const pragmas = options.sqlitePragmas;
62
- const busyTimeout = pragmas?.busyTimeout ?? 5000;
63
- sqlite.run(`PRAGMA busy_timeout = ${busyTimeout}`);
64
- if (pragmas?.journalMode) {
65
- sqlite.run(`PRAGMA journal_mode = ${pragmas.journalMode}`);
66
- }
67
- if (pragmas?.synchronous) {
68
- sqlite.run(`PRAGMA synchronous = ${pragmas.synchronous}`);
69
- }
70
-
71
- const db = new Kysely<any>({
72
- dialect: new BunSqliteDialect({ database: sqlite }),
73
- });
64
+ const db = await createSubprocessDatabase(options);
74
65
 
75
66
  const cache = createCache();
76
67
  const events = createEvents();
@@ -166,12 +157,57 @@ export async function bootstrapSubprocess(
166
157
  }
167
158
 
168
159
  await db.destroy();
169
- sqlite.close();
170
160
  };
171
161
 
172
162
  return { core, manager, db, workflowAdapter, cleanup };
173
163
  }
174
164
 
165
+ async function createSubprocessDatabase(options: SubprocessBootstrapOptions): Promise<Kysely<any>> {
166
+ const dbConfig = options.database;
167
+ if (!dbConfig || dbConfig.type === "sqlite") {
168
+ const sqlitePath = dbConfig?.connectionString ?? options.dbPath;
169
+ if (!sqlitePath) {
170
+ throw new Error("SQLite dbPath or connectionString is required for subprocess workflows");
171
+ }
172
+ const sqlite = new Database(sqlitePath);
173
+ const pragmas = options.sqlitePragmas;
174
+ const busyTimeout = pragmas?.busyTimeout ?? 5000;
175
+ sqlite.run(`PRAGMA busy_timeout = ${busyTimeout}`);
176
+ if (pragmas?.journalMode) {
177
+ sqlite.run(`PRAGMA journal_mode = ${pragmas.journalMode}`);
178
+ }
179
+ if (pragmas?.synchronous) {
180
+ sqlite.run(`PRAGMA synchronous = ${pragmas.synchronous}`);
181
+ }
182
+
183
+ return new Kysely<any>({
184
+ dialect: new BunSqliteDialect({ database: sqlite }),
185
+ });
186
+ }
187
+
188
+ if (dbConfig.type === "postgres") {
189
+ // @ts-ignore optional dependency
190
+ const { Pool: PGPool } = await import("pg");
191
+ return new Kysely<any>({
192
+ dialect: new PostgresDialect({
193
+ pool: new PGPool({ connectionString: dbConfig.connectionString }),
194
+ }),
195
+ });
196
+ }
197
+
198
+ if (dbConfig.type === "mysql") {
199
+ // @ts-ignore optional dependency
200
+ const { createPool: createMySQLPool } = await import("mysql2");
201
+ return new Kysely<any>({
202
+ dialect: new MysqlDialect({
203
+ pool: createMySQLPool(dbConfig.connectionString),
204
+ }),
205
+ });
206
+ }
207
+
208
+ throw new Error(`Unsupported database type: ${dbConfig.type}`);
209
+ }
210
+
175
211
  async function loadConfiguredPlugins(
176
212
  metadata: SubprocessPluginMetadata
177
213
  ): Promise<ConfiguredPlugin[]> {
@@ -20,7 +20,7 @@ interface ExecutorConfig {
20
20
  socketPath?: string;
21
21
  tcpPort?: number;
22
22
  modulePath: string;
23
- dbPath: string;
23
+ dbPath?: string;
24
24
  pluginNames: string[];
25
25
  pluginModulePaths: Record<string, string>;
26
26
  pluginConfigs: Record<string, any>;
@@ -30,6 +30,10 @@ interface ExecutorConfig {
30
30
  synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA";
31
31
  journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
32
32
  };
33
+ database?: {
34
+ type: "sqlite" | "postgres" | "mysql";
35
+ connectionString: string;
36
+ };
33
37
  }
34
38
 
35
39
  // ============================================
@@ -53,6 +57,7 @@ async function main(): Promise<void> {
53
57
  pluginConfigs,
54
58
  coreConfig,
55
59
  sqlitePragmas,
60
+ database,
56
61
  } = config;
57
62
 
58
63
  const socket = await connectToSocket(socketPath, tcpPort);
@@ -76,6 +81,7 @@ async function main(): Promise<void> {
76
81
 
77
82
  const bootstrap = await bootstrapSubprocess({
78
83
  dbPath,
84
+ database,
79
85
  coreConfig,
80
86
  sqlitePragmas,
81
87
  pluginMetadata: {
@@ -756,6 +756,8 @@ export interface WorkflowsConfig {
756
756
  tcpPortRange?: [number, number];
757
757
  /** Database file path (required for isolated workflows) */
758
758
  dbPath?: string;
759
+ /** Database config for isolated workflow subprocesses */
760
+ database?: WorkflowDatabaseConfig;
759
761
  /** Heartbeat timeout in ms (default: 60000) */
760
762
  heartbeatTimeout?: number;
761
763
  /** Timeout waiting for isolated subprocess readiness (ms, default: 10000) */
@@ -768,8 +770,6 @@ export interface WorkflowsConfig {
768
770
  useWatchdog?: boolean;
769
771
  /** Default max concurrent instances per workflow name (0 = unlimited, default: 0) */
770
772
  concurrentWorkflows?: number;
771
- /** Per-workflow concurrency overrides */
772
- concurrentWorkflowsByName?: Record<string, number>;
773
773
  /** Resume strategy for orphaned workflows (default: "blocking") */
774
774
  resumeStrategy?: WorkflowResumeStrategy;
775
775
  }
@@ -782,6 +782,11 @@ export interface SqlitePragmaConfig {
782
782
  journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
783
783
  }
784
784
 
785
+ export interface WorkflowDatabaseConfig {
786
+ type: "sqlite" | "postgres" | "mysql";
787
+ connectionString: string;
788
+ }
789
+
785
790
  /** Options for registering a workflow */
786
791
  export interface WorkflowRegisterOptions {
787
792
  /**
@@ -872,13 +877,13 @@ class WorkflowsImpl implements Workflows {
872
877
  private socketDir: string;
873
878
  private tcpPortRange: [number, number];
874
879
  private dbPath?: string;
880
+ private databaseConfig?: WorkflowDatabaseConfig;
875
881
  private heartbeatTimeoutMs: number;
876
882
  private readyTimeoutMs: number;
877
883
  private killGraceMs: number;
878
884
  private sqlitePragmas?: SqlitePragmaConfig;
879
885
  private useWatchdog: boolean;
880
886
  private concurrentWorkflows: number;
881
- private concurrentWorkflowsByName: Record<string, number>;
882
887
  private workflowConcurrencyOverrides = new Map<string, number>();
883
888
  private resumeStrategy!: WorkflowResumeStrategy;
884
889
  private workflowModulePaths = new Map<string, string>();
@@ -912,13 +917,13 @@ class WorkflowsImpl implements Workflows {
912
917
  this.socketDir = config.socketDir ?? "/tmp/donkeylabs-workflows";
913
918
  this.tcpPortRange = config.tcpPortRange ?? [49152, 65535];
914
919
  this.dbPath = config.dbPath;
920
+ this.databaseConfig = config.database;
915
921
  this.heartbeatTimeoutMs = config.heartbeatTimeout ?? 60000;
916
922
  this.readyTimeoutMs = config.readyTimeout ?? 10000;
917
923
  this.killGraceMs = config.killGraceMs ?? 5000;
918
924
  this.sqlitePragmas = config.sqlitePragmas;
919
925
  this.useWatchdog = config.useWatchdog ?? false;
920
926
  this.concurrentWorkflows = config.concurrentWorkflows ?? 0;
921
- this.concurrentWorkflowsByName = config.concurrentWorkflowsByName ?? {};
922
927
  this.resumeStrategy = config.resumeStrategy ?? "blocking";
923
928
  }
924
929
 
@@ -952,6 +957,9 @@ class WorkflowsImpl implements Workflows {
952
957
  }
953
958
 
954
959
  async resolveDbPath(): Promise<void> {
960
+ if (this.databaseConfig && this.databaseConfig.type !== "sqlite") {
961
+ return;
962
+ }
955
963
  if (this.dbPath) return;
956
964
  if (!this.core?.db) return;
957
965
 
@@ -1064,8 +1072,9 @@ class WorkflowsImpl implements Workflows {
1064
1072
  // Start execution (isolated or inline based on definition.isolated)
1065
1073
  const isIsolated = definition.isolated !== false;
1066
1074
  const modulePath = this.workflowModulePaths.get(workflowName);
1075
+ const canIsolate = Boolean(this.dbPath || this.databaseConfig);
1067
1076
 
1068
- if (isIsolated && modulePath && this.dbPath) {
1077
+ if (isIsolated && modulePath && canIsolate) {
1069
1078
  // Execute in isolated subprocess
1070
1079
  await this.executeIsolatedWorkflow(instance.id, definition, input, modulePath);
1071
1080
  } else {
@@ -1074,10 +1083,10 @@ class WorkflowsImpl implements Workflows {
1074
1083
  console.warn(
1075
1084
  `[Workflows] Workflow "${workflowName}" falling back to inline execution (no modulePath)`
1076
1085
  );
1077
- } else if (isIsolated && modulePath && !this.dbPath) {
1086
+ } else if (isIsolated && modulePath && !canIsolate) {
1078
1087
  console.warn(
1079
1088
  `[Workflows] Workflow "${workflowName}" falling back to inline execution (dbPath could not be auto-detected). ` +
1080
- `Set workflows.dbPath in your server config to enable isolated execution.`
1089
+ `Set workflows.dbPath or workflows.database in your server config to enable isolated execution.`
1081
1090
  );
1082
1091
  }
1083
1092
  this.startInlineWorkflow(instance.id, definition);
@@ -1517,6 +1526,7 @@ class WorkflowsImpl implements Workflows {
1517
1526
  pluginConfigs,
1518
1527
  coreConfig,
1519
1528
  sqlitePragmas: this.sqlitePragmas,
1529
+ database: this.databaseConfig,
1520
1530
  };
1521
1531
 
1522
1532
  // Spawn the subprocess
@@ -2021,9 +2031,6 @@ class WorkflowsImpl implements Workflows {
2021
2031
  if (this.workflowConcurrencyOverrides.has(workflowName)) {
2022
2032
  return this.workflowConcurrencyOverrides.get(workflowName) ?? 0;
2023
2033
  }
2024
- if (this.concurrentWorkflowsByName[workflowName] !== undefined) {
2025
- return this.concurrentWorkflowsByName[workflowName] ?? 0;
2026
- }
2027
2034
  return this.concurrentWorkflows;
2028
2035
  }
2029
2036
 
package/src/server.ts CHANGED
@@ -69,6 +69,11 @@ export interface ServerConfig {
69
69
  /** Maximum port attempts if port is in use. Default: 5 */
70
70
  maxPortAttempts?: number;
71
71
  db: CoreServices["db"];
72
+ /** Serializable database config for subprocesses */
73
+ database?: {
74
+ type: "sqlite" | "postgres" | "mysql";
75
+ connectionString: string;
76
+ };
72
77
  config?: Record<string, any>;
73
78
  /** Auto-generate client types on startup in dev mode */
74
79
  generateTypes?: TypeGenerationConfig;
@@ -314,6 +319,7 @@ export class AppServer {
314
319
  jobs,
315
320
  sse,
316
321
  adapter: workflowAdapter,
322
+ database: options.database,
317
323
  useWatchdog: options.watchdog?.enabled ? true : options.workflows?.useWatchdog,
318
324
  });
319
325