@donkeylabs/server 2.0.30 → 2.0.32

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
@@ -89,6 +89,26 @@ const instanceId = await ctx.core.workflows.start("process-order", {
89
89
  });
90
90
  ```
91
91
 
92
+ ### Concurrency Guard
93
+
94
+ Limit concurrent instances per workflow name:
95
+
96
+ ```ts
97
+ const server = new AppServer({
98
+ db,
99
+ workflows: {
100
+ concurrentWorkflows: 1, // default for all workflows (0 = unlimited)
101
+ concurrentWorkflowsByName: {
102
+ testWorkflow: 1,
103
+ ingestionWorkflow: 1,
104
+ },
105
+ },
106
+ });
107
+
108
+ // Or per-register override
109
+ ctx.core.workflows.register(orderWorkflow, { maxConcurrent: 1 });
110
+ ```
111
+
92
112
  ### 3. Track Progress
93
113
 
94
114
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "2.0.30",
3
+ "version": "2.0.32",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
@@ -766,6 +766,10 @@ export interface WorkflowsConfig {
766
766
  sqlitePragmas?: SqlitePragmaConfig;
767
767
  /** Disable in-process watchdog timers (use external watchdog instead) */
768
768
  useWatchdog?: boolean;
769
+ /** Default max concurrent instances per workflow name (0 = unlimited, default: 0) */
770
+ concurrentWorkflows?: number;
771
+ /** Per-workflow concurrency overrides */
772
+ concurrentWorkflowsByName?: Record<string, number>;
769
773
  /** Resume strategy for orphaned workflows (default: "blocking") */
770
774
  resumeStrategy?: WorkflowResumeStrategy;
771
775
  }
@@ -794,6 +798,8 @@ export interface WorkflowRegisterOptions {
794
798
  * workflows.register(myWorkflow, { modulePath: import.meta.url });
795
799
  */
796
800
  modulePath?: string;
801
+ /** Max concurrent instances for this workflow (overrides defaults) */
802
+ maxConcurrent?: number;
797
803
  }
798
804
 
799
805
  export interface Workflows {
@@ -871,6 +877,9 @@ class WorkflowsImpl implements Workflows {
871
877
  private killGraceMs: number;
872
878
  private sqlitePragmas?: SqlitePragmaConfig;
873
879
  private useWatchdog: boolean;
880
+ private concurrentWorkflows: number;
881
+ private concurrentWorkflowsByName: Record<string, number>;
882
+ private workflowConcurrencyOverrides = new Map<string, number>();
874
883
  private resumeStrategy!: WorkflowResumeStrategy;
875
884
  private workflowModulePaths = new Map<string, string>();
876
885
  private isolatedProcesses = new Map<string, IsolatedProcessInfo>();
@@ -908,6 +917,8 @@ class WorkflowsImpl implements Workflows {
908
917
  this.killGraceMs = config.killGraceMs ?? 5000;
909
918
  this.sqlitePragmas = config.sqlitePragmas;
910
919
  this.useWatchdog = config.useWatchdog ?? false;
920
+ this.concurrentWorkflows = config.concurrentWorkflows ?? 0;
921
+ this.concurrentWorkflowsByName = config.concurrentWorkflowsByName ?? {};
911
922
  this.resumeStrategy = config.resumeStrategy ?? "blocking";
912
923
  }
913
924
 
@@ -998,6 +1009,10 @@ class WorkflowsImpl implements Workflows {
998
1009
  }
999
1010
 
1000
1011
  this.definitions.set(definition.name, definition);
1012
+
1013
+ if (options?.maxConcurrent !== undefined) {
1014
+ this.workflowConcurrencyOverrides.set(definition.name, options.maxConcurrent);
1015
+ }
1001
1016
  }
1002
1017
 
1003
1018
  async start<T = any>(workflowName: string, input: T): Promise<string> {
@@ -1006,6 +1021,17 @@ class WorkflowsImpl implements Workflows {
1006
1021
  throw new Error(`Workflow "${workflowName}" is not registered`);
1007
1022
  }
1008
1023
 
1024
+ const limit = this.getConcurrencyLimit(workflowName);
1025
+ if (limit > 0) {
1026
+ const running = await this.adapter.getInstancesByWorkflow(workflowName, "running");
1027
+ const pending = await this.adapter.getInstancesByWorkflow(workflowName, "pending");
1028
+ if (running.length + pending.length >= limit) {
1029
+ throw new Error(
1030
+ `Workflow "${workflowName}" has reached its concurrency limit (${limit})`
1031
+ );
1032
+ }
1033
+ }
1034
+
1009
1035
  const instance = await this.adapter.createInstance({
1010
1036
  workflowName,
1011
1037
  status: "pending",
@@ -1991,6 +2017,16 @@ class WorkflowsImpl implements Workflows {
1991
2017
  await this.adapter.updateInstance(instanceId, { metadata });
1992
2018
  }
1993
2019
 
2020
+ private getConcurrencyLimit(workflowName: string): number {
2021
+ if (this.workflowConcurrencyOverrides.has(workflowName)) {
2022
+ return this.workflowConcurrencyOverrides.get(workflowName) ?? 0;
2023
+ }
2024
+ if (this.concurrentWorkflowsByName[workflowName] !== undefined) {
2025
+ return this.concurrentWorkflowsByName[workflowName] ?? 0;
2026
+ }
2027
+ return this.concurrentWorkflows;
2028
+ }
2029
+
1994
2030
  private async markOrphanedAsFailed(
1995
2031
  instances: WorkflowInstance[],
1996
2032
  reason: string