@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
|
@@ -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({
|
package/src/core/workflows.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
|
1026
|
+
`[Workflows] Failed to resume workflow ${instance.id}:`,
|
|
1005
1027
|
error instanceof Error ? error.message : String(error)
|
|
1006
1028
|
);
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
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
|
-
|
|
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();
|