@nanobpm/workflow 0.15.1 → 0.16.0

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/README.md CHANGED
@@ -105,6 +105,19 @@ const { processInstanceKey } = await client.start(onboarding, {});
105
105
  await client.signal(onboarding, "approved", userId, { by: "alice" });
106
106
  ```
107
107
 
108
+ A long-running step (transcription, an agent call, a multi-GB upload) can
109
+ outlive the engine's default job lock: the lease expires mid-handler, the
110
+ engine re-activates the job, and a second worker runs it again. Declare the
111
+ lock timeout per step with `w.run(name, handler, { timeoutMs })` (overrides the
112
+ `Worker`'s worker-wide `jobTimeoutMs`, default 30s, for that step's job type):
113
+
114
+ ```ts
115
+ const flow = defineFlow("uploads", (w) => {
116
+ w.run("transcode", transcode, { timeoutMs: 3_600_000 }); // 1h lock for this step
117
+ w.run("notify", notify);
118
+ });
119
+ ```
120
+
108
121
  A human-in-the-loop **approval gate** with a form, an assignment, and an output
109
122
  mapping that stamps the decision back onto a process variable:
110
123
 
@@ -45,6 +45,12 @@ export interface FlowBuilder<C extends object = object> {
45
45
  */
46
46
  run<K extends string>(name: K, handler: TypedHandler<VarsOf<C, K>, ResultOf<C, K>>, opts?: {
47
47
  io?: HumanIoMapping;
48
+ /** Job lock timeout, ms, for this step's derived worker (issue #596): how long the
49
+ * engine leases an activated job before re-activating it elsewhere. Set it above the
50
+ * handler's worst-case runtime for long steps (transcription, an agent call, a
51
+ * multi-GB upload) so the lock cannot expire mid-handler and the job run twice.
52
+ * Overrides the `Worker`'s worker-wide `jobTimeoutMs` for this step's job type. */
53
+ timeoutMs?: number;
48
54
  }): FlowBuilder<C>;
49
55
  /**
50
56
  * A durable activity served by a worker OUTSIDE this program (a BPMN service
@@ -6,6 +6,7 @@ declare module "../types.js" {
6
6
  name: string;
7
7
  envelopes?: NodeEnvelopes;
8
8
  io?: HumanIoMapping;
9
+ timeoutMs?: number;
9
10
  };
10
11
  }
11
12
  }
package/dist/nodes/run.js CHANGED
@@ -11,8 +11,18 @@ registerNodeKind("run", {
11
11
  if (typeof handler !== "function")
12
12
  throw new Error(`run("${name}") needs a handler function`);
13
13
  assertIoMapping(`run("${name}")`, opts?.io);
14
+ const timeoutMs = opts?.timeoutMs;
15
+ if (timeoutMs !== undefined && (!Number.isInteger(timeoutMs) || timeoutMs <= 0)) {
16
+ throw new Error(`run("${name}"): timeoutMs must be a positive integer (ms)`);
17
+ }
14
18
  api.handlers[name] = handler;
15
- api.out.push({ kind: "run", name, envelopes: api.contractEnvelopes(name), io: opts?.io });
19
+ api.out.push({
20
+ kind: "run",
21
+ name,
22
+ envelopes: api.contractEnvelopes(name),
23
+ io: opts?.io,
24
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
25
+ });
16
26
  return api.self();
17
27
  },
18
28
  emit: (node, incoming, _loop, api) => {
package/dist/worker.d.ts CHANGED
@@ -54,6 +54,9 @@ export declare class Worker {
54
54
  private readonly onError?;
55
55
  /** job type → { workflowId, handle } */
56
56
  private readonly routes;
57
+ /** Job lock timeout (ms) per derived job type, from a `run` step's `{ timeoutMs }`
58
+ * (issue #596). Overrides the worker-wide `jobTimeoutMs` for that type. */
59
+ private readonly typeTimeoutMs;
57
60
  private running;
58
61
  private workers;
59
62
  constructor(opts: WorkerOptions);
package/dist/worker.js CHANGED
@@ -30,6 +30,9 @@ export class Worker {
30
30
  onError;
31
31
  /** job type → { workflowId, handle } */
32
32
  routes = new Map();
33
+ /** Job lock timeout (ms) per derived job type, from a `run` step's `{ timeoutMs }`
34
+ * (issue #596). Overrides the worker-wide `jobTimeoutMs` for that type. */
35
+ typeTimeoutMs = new Map();
33
36
  running = false;
34
37
  workers = [];
35
38
  constructor(opts) {
@@ -87,6 +90,10 @@ export class Worker {
87
90
  return { variables };
88
91
  },
89
92
  });
93
+ // A step-declared job lock timeout (issue #596) applies to this step's derived
94
+ // job type, overriding the worker-wide `jobTimeoutMs` at `start()`.
95
+ if (s.timeoutMs !== undefined)
96
+ this.typeTimeoutMs.set(jobType(wf.id, s.name), s.timeoutMs);
90
97
  });
91
98
  }
92
99
  }
@@ -141,7 +148,8 @@ export class Worker {
141
148
  jobType: type,
142
149
  workerName: this.name,
143
150
  maxParallelJobs: this.maxParallelJobs,
144
- jobTimeoutMs: this.jobTimeoutMs,
151
+ // A step-declared `{ timeoutMs }` (issue #596) wins over the worker-wide default.
152
+ jobTimeoutMs: this.typeTimeoutMs.get(type) ?? this.jobTimeoutMs,
145
153
  pollTimeoutMs: this.pollTimeoutMs,
146
154
  autoStart: false,
147
155
  jobHandler: (job) => this.handleJob(type, route, job),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/workflow",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Code-first durable orchestration for nanobpmn (ADR 0044/0045). Author a durable workflow as a declarative flow — `defineFlow(id, w => { w.run(...); w.signal(...); w.task(...) })` — and the SDK derives the executable BPMN model, the job types, and the message/correlation wiring, then hosts a generic worker. `w.run` is a local step, `w.signal` is a durable human-in-the-loop wait, `w.task` is a step serviced by an external worker. `defineFlow` is the single code-first surface; the imperative `defineWorkflow` replay function is experimental/internal, not a recommended API. No diagram, no task-type wiring, no correlation plumbing authored by hand — and you can optionally auto-generate a diagram (DI) for the derived model with `declarativeToLayoutedBpmn` via the optional `bpmn-auto-layout` peer dependency. Talks to a running nanobpmn gateway through @nanobpm/nano-sdk (the shared engine-transport spine — ADR 0055), which negotiates the Falcon protocol on a Nano server and REST elsewhere.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -102,7 +102,15 @@ export interface FlowBuilder<C extends object = object> {
102
102
  run<K extends string>(
103
103
  name: K,
104
104
  handler: TypedHandler<VarsOf<C, K>, ResultOf<C, K>>,
105
- opts?: { io?: HumanIoMapping },
105
+ opts?: {
106
+ io?: HumanIoMapping;
107
+ /** Job lock timeout, ms, for this step's derived worker (issue #596): how long the
108
+ * engine leases an activated job before re-activating it elsewhere. Set it above the
109
+ * handler's worst-case runtime for long steps (transcription, an agent call, a
110
+ * multi-GB upload) so the lock cannot expire mid-handler and the job run twice.
111
+ * Overrides the `Worker`'s worker-wide `jobTimeoutMs` for this step's job type. */
112
+ timeoutMs?: number;
113
+ },
106
114
  ): FlowBuilder<C>;
107
115
  /**
108
116
  * A durable activity served by a worker OUTSIDE this program (a BPMN service
package/src/nodes/run.ts CHANGED
@@ -11,17 +11,27 @@ import { registerNodeKind } from "./registry.js";
11
11
 
12
12
  declare module "../types.js" {
13
13
  interface FlowNodeRegistry {
14
- run: { kind: "run"; name: string; envelopes?: NodeEnvelopes; io?: HumanIoMapping };
14
+ run: { kind: "run"; name: string; envelopes?: NodeEnvelopes; io?: HumanIoMapping; timeoutMs?: number };
15
15
  }
16
16
  }
17
17
 
18
18
  registerNodeKind("run", {
19
- build: (api) => (name: string, handler: StepHandler, opts?: { io?: HumanIoMapping }) => {
19
+ build: (api) => (name: string, handler: StepHandler, opts?: { io?: HumanIoMapping; timeoutMs?: number }) => {
20
20
  api.claim(name);
21
21
  if (typeof handler !== "function") throw new Error(`run("${name}") needs a handler function`);
22
22
  assertIoMapping(`run("${name}")`, opts?.io);
23
+ const timeoutMs = opts?.timeoutMs;
24
+ if (timeoutMs !== undefined && (!Number.isInteger(timeoutMs) || timeoutMs <= 0)) {
25
+ throw new Error(`run("${name}"): timeoutMs must be a positive integer (ms)`);
26
+ }
23
27
  api.handlers[name] = handler;
24
- api.out.push({ kind: "run", name, envelopes: api.contractEnvelopes(name), io: opts?.io });
28
+ api.out.push({
29
+ kind: "run",
30
+ name,
31
+ envelopes: api.contractEnvelopes(name),
32
+ io: opts?.io,
33
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
34
+ });
25
35
  return api.self();
26
36
  },
27
37
  emit: (node, incoming, _loop, api) => {
package/src/worker.ts CHANGED
@@ -84,6 +84,9 @@ export class Worker {
84
84
  private readonly onError?: WorkerOptions["onError"];
85
85
  /** job type → { workflowId, handle } */
86
86
  private readonly routes = new Map<string, { workflowId: string; handle: Handler }>();
87
+ /** Job lock timeout (ms) per derived job type, from a `run` step's `{ timeoutMs }`
88
+ * (issue #596). Overrides the worker-wide `jobTimeoutMs` for that type. */
89
+ private readonly typeTimeoutMs = new Map<string, number>();
87
90
  private running = false;
88
91
  private workers: NanoJobWorker[] = [];
89
92
 
@@ -142,6 +145,9 @@ export class Worker {
142
145
  return { variables };
143
146
  },
144
147
  });
148
+ // A step-declared job lock timeout (issue #596) applies to this step's derived
149
+ // job type, overriding the worker-wide `jobTimeoutMs` at `start()`.
150
+ if (s.timeoutMs !== undefined) this.typeTimeoutMs.set(jobType(wf.id, s.name), s.timeoutMs);
145
151
  });
146
152
  }
147
153
  }
@@ -200,7 +206,8 @@ export class Worker {
200
206
  jobType: type,
201
207
  workerName: this.name,
202
208
  maxParallelJobs: this.maxParallelJobs,
203
- jobTimeoutMs: this.jobTimeoutMs,
209
+ // A step-declared `{ timeoutMs }` (issue #596) wins over the worker-wide default.
210
+ jobTimeoutMs: this.typeTimeoutMs.get(type) ?? this.jobTimeoutMs,
204
211
  pollTimeoutMs: this.pollTimeoutMs,
205
212
  autoStart: false,
206
213
  jobHandler: (job) => this.handleJob(type, route, job),