@donkeylabs/server 2.0.33 → 2.0.35

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
@@ -334,6 +334,10 @@ You can tune subprocess termination and SQLite pragmas used by isolated workflow
334
334
  ```ts
335
335
  const server = new AppServer({
336
336
  db,
337
+ database: {
338
+ type: "postgres",
339
+ connectionString: process.env.DATABASE_URL!,
340
+ },
337
341
  watchdog: {
338
342
  enabled: true,
339
343
  intervalMs: 5000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "2.0.33",
3
+ "version": "2.0.35",
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) */
@@ -780,6 +782,11 @@ export interface SqlitePragmaConfig {
780
782
  journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
781
783
  }
782
784
 
785
+ export interface WorkflowDatabaseConfig {
786
+ type: "sqlite" | "postgres" | "mysql";
787
+ connectionString: string;
788
+ }
789
+
783
790
  /** Options for registering a workflow */
784
791
  export interface WorkflowRegisterOptions {
785
792
  /**
@@ -870,6 +877,7 @@ class WorkflowsImpl implements Workflows {
870
877
  private socketDir: string;
871
878
  private tcpPortRange: [number, number];
872
879
  private dbPath?: string;
880
+ private databaseConfig?: WorkflowDatabaseConfig;
873
881
  private heartbeatTimeoutMs: number;
874
882
  private readyTimeoutMs: number;
875
883
  private killGraceMs: number;
@@ -909,6 +917,7 @@ class WorkflowsImpl implements Workflows {
909
917
  this.socketDir = config.socketDir ?? "/tmp/donkeylabs-workflows";
910
918
  this.tcpPortRange = config.tcpPortRange ?? [49152, 65535];
911
919
  this.dbPath = config.dbPath;
920
+ this.databaseConfig = config.database;
912
921
  this.heartbeatTimeoutMs = config.heartbeatTimeout ?? 60000;
913
922
  this.readyTimeoutMs = config.readyTimeout ?? 10000;
914
923
  this.killGraceMs = config.killGraceMs ?? 5000;
@@ -948,6 +957,9 @@ class WorkflowsImpl implements Workflows {
948
957
  }
949
958
 
950
959
  async resolveDbPath(): Promise<void> {
960
+ if (this.databaseConfig && this.databaseConfig.type !== "sqlite") {
961
+ return;
962
+ }
951
963
  if (this.dbPath) return;
952
964
  if (!this.core?.db) return;
953
965
 
@@ -1060,8 +1072,9 @@ class WorkflowsImpl implements Workflows {
1060
1072
  // Start execution (isolated or inline based on definition.isolated)
1061
1073
  const isIsolated = definition.isolated !== false;
1062
1074
  const modulePath = this.workflowModulePaths.get(workflowName);
1075
+ const canIsolate = Boolean(this.dbPath || this.databaseConfig);
1063
1076
 
1064
- if (isIsolated && modulePath && this.dbPath) {
1077
+ if (isIsolated && modulePath && canIsolate) {
1065
1078
  // Execute in isolated subprocess
1066
1079
  await this.executeIsolatedWorkflow(instance.id, definition, input, modulePath);
1067
1080
  } else {
@@ -1070,10 +1083,10 @@ class WorkflowsImpl implements Workflows {
1070
1083
  console.warn(
1071
1084
  `[Workflows] Workflow "${workflowName}" falling back to inline execution (no modulePath)`
1072
1085
  );
1073
- } else if (isIsolated && modulePath && !this.dbPath) {
1086
+ } else if (isIsolated && modulePath && !canIsolate) {
1074
1087
  console.warn(
1075
1088
  `[Workflows] Workflow "${workflowName}" falling back to inline execution (dbPath could not be auto-detected). ` +
1076
- `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.`
1077
1090
  );
1078
1091
  }
1079
1092
  this.startInlineWorkflow(instance.id, definition);
@@ -1513,6 +1526,7 @@ class WorkflowsImpl implements Workflows {
1513
1526
  pluginConfigs,
1514
1527
  coreConfig,
1515
1528
  sqlitePragmas: this.sqlitePragmas,
1529
+ database: this.databaseConfig,
1516
1530
  };
1517
1531
 
1518
1532
  // Spawn the subprocess
package/src/core.ts CHANGED
@@ -544,10 +544,15 @@ export class PluginManager {
544
544
  * This table tracks which migrations have been applied for each plugin.
545
545
  */
546
546
  private async ensureMigrationsTable(): Promise<void> {
547
- await this.core.db.schema
547
+ const table = this.core.db.schema
548
548
  .createTable("__donkeylabs_migrations__")
549
- .ifNotExists()
550
- .addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
549
+ .ifNotExists();
550
+
551
+ const withId = this.isPostgresDialect()
552
+ ? table.addColumn("id", "serial", (col) => col.primaryKey())
553
+ : table.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement());
554
+
555
+ await withId
551
556
  .addColumn("plugin_name", "text", (col) => col.notNull())
552
557
  .addColumn("migration_name", "text", (col) => col.notNull())
553
558
  .addColumn("executed_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`))
@@ -559,6 +564,16 @@ export class PluginManager {
559
564
  ON __donkeylabs_migrations__(plugin_name, migration_name)`.execute(this.core.db);
560
565
  }
561
566
 
567
+ private isPostgresDialect(): boolean {
568
+ try {
569
+ const adapter = (this.core.db as any).getExecutor?.().adapter;
570
+ const name = adapter?.constructor?.name?.toLowerCase() ?? "";
571
+ return name.includes("postgres");
572
+ } catch {
573
+ return false;
574
+ }
575
+ }
576
+
562
577
  /**
563
578
  * Checks if a migration has already been applied for a specific plugin.
564
579
  */
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