@nanobpm/workflow 0.3.0 → 0.5.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 +23 -3
- package/dist/client.d.ts +25 -2
- package/dist/client.js +47 -3
- package/dist/declarative.d.ts +17 -7
- package/dist/declarative.js +20 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/layout.js +3 -2
- package/dist/types.d.ts +1 -0
- package/dist/xml.d.ts +1 -0
- package/dist/xml.js +15 -1
- package/package.json +5 -5
- package/src/bpmn-auto-layout.d.ts +5 -5
- package/src/client.ts +51 -3
- package/src/declarative.ts +28 -13
- package/src/index.ts +1 -1
- package/src/layout.ts +3 -2
- package/src/types.ts +1 -1
- package/src/xml.ts +17 -1
package/README.md
CHANGED
|
@@ -34,7 +34,9 @@ Both compile to the same engine durability; pick per workflow.
|
|
|
34
34
|
### Declarative (with control flow, human-in-the-loop signals, and typed I/O)
|
|
35
35
|
|
|
36
36
|
**The recommended surface.** Describe the flow as a **tree of nodes**: `w.run` (a
|
|
37
|
-
locally-hosted service task), `w.task` (an external-worker service task
|
|
37
|
+
locally-hosted service task), `w.task` (an external-worker service task — job
|
|
38
|
+
type `${flowId}:${name}` by default, or pass `w.task(name, { jobType })` to
|
|
39
|
+
target an existing worker pool, e.g. a `senior:pr-review` agent token), and
|
|
38
40
|
`w.signal` (a durable message catch that resumes via a correlated message — the
|
|
39
41
|
human-in-the-loop path), composed with control-flow combinators:
|
|
40
42
|
|
|
@@ -178,10 +180,28 @@ once.
|
|
|
178
180
|
| `defineWorkflow(id, orchFn)` | Imperative (replayed) workflow — experimental/internal. |
|
|
179
181
|
| `defineFlow(id, [contracts,] build)` | Declarative flow: `run`/`task`/`signal` + `switch`/`branch`/`loop`/`break`/`continue`, with an optional typed contracts map. |
|
|
180
182
|
| `envelope(name, fields)` | A typed data envelope; lifted into the model as a `nano:shape` + `dataEnvelope` wiring. |
|
|
181
|
-
| `externalJobTypes(flow)` | The
|
|
183
|
+
| `externalJobTypes(flow)` | The job types of a flow's external `task` steps (each overridable per-step via `w.task(name, { jobType })`). |
|
|
182
184
|
| `WorkflowClient` | `deploy`, `start`, `signal`, `getInstance` over REST v2. |
|
|
183
185
|
| `Worker` | Generic job runtime; routes job types → handlers, hosts the replay loop. |
|
|
184
|
-
| `toBpmn(workflow)` | The
|
|
186
|
+
| `toBpmn(workflow)` | The DI-less semantic BPMN the engine runs (for inspection / deployment). |
|
|
187
|
+
| `toDeployableBpmn(workflow, { layout? })` | The deployable BPMN **with an auto-generated diagram (DI)** so the deployed model is inspectable; what `deploy` sends by default. |
|
|
188
|
+
| `layoutBpmn(xml)` / `declarativeToLayoutedBpmn(flow)` | Add DI to a model with `bpmn-auto-layout`. |
|
|
189
|
+
|
|
190
|
+
### Inspectable deployments (diagram layout)
|
|
191
|
+
|
|
192
|
+
`client.deploy(flow)` deploys the model **with an auto-generated diagram
|
|
193
|
+
interchange (DI)** so it opens rendered and inspectable in a modeller/Operate
|
|
194
|
+
rather than as a blank canvas. DI is generated with the optional peer dependency
|
|
195
|
+
[`bpmn-auto-layout`](https://www.npmjs.com/package/bpmn-auto-layout):
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
npm i bpmn-auto-layout
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
If it is not installed, `deploy` degrades gracefully — it warns once and deploys
|
|
202
|
+
the DI-less model. Pass `deploy(flow, { layout: false })` to skip layout
|
|
203
|
+
deliberately. The semantic model stays authoritative; DI is derived and
|
|
204
|
+
regenerable.
|
|
185
205
|
|
|
186
206
|
See [ADR 0044](../docs/adr/0044-code-first-durable-orchestration.md) for the design
|
|
187
207
|
rationale and [ADR 0047](../docs/adr/0047-declarative-flow-control-and-typed-envelopes.md)
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import type { DeclarativeFlow, DeployResult, Job, JsonObject, StartResult, Workflow } from "./types.js";
|
|
2
2
|
/** Render a workflow (either surface) to its executable BPMN model. */
|
|
3
3
|
export declare function toBpmn(wf: Workflow): string;
|
|
4
|
+
/**
|
|
5
|
+
* Render a workflow to *deployable* BPMN — the executable model plus diagram
|
|
6
|
+
* interchange (DI), auto-generated with `bpmn-auto-layout` — so the deployed
|
|
7
|
+
* process opens rendered and inspectable in a modeller/Operate rather than as a
|
|
8
|
+
* blank canvas. The semantic model stays authoritative; DI is derived.
|
|
9
|
+
*
|
|
10
|
+
* DI generation needs the optional peer dependency `bpmn-auto-layout`. When it is
|
|
11
|
+
* absent, this degrades gracefully: it warns once and returns the DI-less model
|
|
12
|
+
* so `deploy` still works everywhere. Pass `{ layout: false }` to skip layout
|
|
13
|
+
* deliberately. A genuine layout failure (the dep is installed but errors) is
|
|
14
|
+
* surfaced, not swallowed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toDeployableBpmn(wf: Workflow, opts?: {
|
|
17
|
+
layout?: boolean;
|
|
18
|
+
}): Promise<string>;
|
|
4
19
|
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
5
20
|
export interface WorkflowClientOptions {
|
|
6
21
|
/** Base URL of the nanobpmn gateway, e.g. `http://localhost:8080`. */
|
|
@@ -29,8 +44,16 @@ export declare class WorkflowClient {
|
|
|
29
44
|
/** Perform a request, throwing WorkflowError on transport error or !ok, and
|
|
30
45
|
* return the raw Response (for endpoints with an empty/no-content body). */
|
|
31
46
|
private send;
|
|
32
|
-
/**
|
|
33
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Deploy a workflow's derived BPMN model. The deployed model includes
|
|
49
|
+
* auto-generated diagram interchange (DI) so it is inspectable in a
|
|
50
|
+
* modeller/Operate; pass `{ layout: false }` to deploy the DI-less semantic
|
|
51
|
+
* model. DI needs the optional `bpmn-auto-layout` dependency — see
|
|
52
|
+
* `toDeployableBpmn` for the graceful-degradation behaviour when it is absent.
|
|
53
|
+
*/
|
|
54
|
+
deploy(wf: Workflow, opts?: {
|
|
55
|
+
layout?: boolean;
|
|
56
|
+
}): Promise<DeployResult>;
|
|
34
57
|
/**
|
|
35
58
|
* Start a workflow instance. For imperative workflows the engine variables are
|
|
36
59
|
* seeded with `{ input, journal: {}, wfDone: false }` (the replay state); for
|
package/dist/client.js
CHANGED
|
@@ -3,12 +3,50 @@
|
|
|
3
3
|
// low-level job activate/complete/fail used by the Worker runtime.
|
|
4
4
|
import { declarativeToBpmn, walkNodes } from "./declarative.js";
|
|
5
5
|
import { imperativeToBpmn } from "./imperative.js";
|
|
6
|
+
import { layoutBpmn } from "./layout.js";
|
|
6
7
|
import { assertWorkflowIds, messageName } from "./xml.js";
|
|
7
8
|
/** Render a workflow (either surface) to its executable BPMN model. */
|
|
8
9
|
export function toBpmn(wf) {
|
|
9
10
|
assertWorkflowIds(wf);
|
|
10
11
|
return wf.kind === "imperative" ? imperativeToBpmn(wf) : declarativeToBpmn(wf);
|
|
11
12
|
}
|
|
13
|
+
let warnedNoLayout = false;
|
|
14
|
+
/**
|
|
15
|
+
* Render a workflow to *deployable* BPMN — the executable model plus diagram
|
|
16
|
+
* interchange (DI), auto-generated with `bpmn-auto-layout` — so the deployed
|
|
17
|
+
* process opens rendered and inspectable in a modeller/Operate rather than as a
|
|
18
|
+
* blank canvas. The semantic model stays authoritative; DI is derived.
|
|
19
|
+
*
|
|
20
|
+
* DI generation needs the optional peer dependency `bpmn-auto-layout`. When it is
|
|
21
|
+
* absent, this degrades gracefully: it warns once and returns the DI-less model
|
|
22
|
+
* so `deploy` still works everywhere. Pass `{ layout: false }` to skip layout
|
|
23
|
+
* deliberately. A genuine layout failure (the dep is installed but errors) is
|
|
24
|
+
* surfaced, not swallowed.
|
|
25
|
+
*/
|
|
26
|
+
export async function toDeployableBpmn(wf, opts = {}) {
|
|
27
|
+
const semantic = toBpmn(wf);
|
|
28
|
+
if (opts.layout === false)
|
|
29
|
+
return semantic;
|
|
30
|
+
try {
|
|
31
|
+
return await layoutBpmn(semantic);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
// Only fall back for the "optional dep not installed" case (layoutBpmn wraps
|
|
35
|
+
// it with an ERR_MODULE_NOT_FOUND cause). Any other failure is a real layout
|
|
36
|
+
// problem and must surface rather than silently deploy an uninspectable model.
|
|
37
|
+
const cause = e?.cause;
|
|
38
|
+
if (cause?.code !== "ERR_MODULE_NOT_FOUND")
|
|
39
|
+
throw e;
|
|
40
|
+
if (!warnedNoLayout) {
|
|
41
|
+
warnedNoLayout = true;
|
|
42
|
+
console.warn(`[@nanobpm/workflow] Deploying "${wf.id}" without a diagram: the optional ` +
|
|
43
|
+
'"bpmn-auto-layout" dependency is not installed, so the model will be ' +
|
|
44
|
+
"uninspectable in a modeller/Operate. Install it for diagram layout: " +
|
|
45
|
+
"npm i bpmn-auto-layout");
|
|
46
|
+
}
|
|
47
|
+
return semantic;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
12
50
|
export class WorkflowError extends Error {
|
|
13
51
|
status;
|
|
14
52
|
body;
|
|
@@ -51,9 +89,15 @@ export class WorkflowClient {
|
|
|
51
89
|
}
|
|
52
90
|
return res;
|
|
53
91
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Deploy a workflow's derived BPMN model. The deployed model includes
|
|
94
|
+
* auto-generated diagram interchange (DI) so it is inspectable in a
|
|
95
|
+
* modeller/Operate; pass `{ layout: false }` to deploy the DI-less semantic
|
|
96
|
+
* model. DI needs the optional `bpmn-auto-layout` dependency — see
|
|
97
|
+
* `toDeployableBpmn` for the graceful-degradation behaviour when it is absent.
|
|
98
|
+
*/
|
|
99
|
+
async deploy(wf, opts = {}) {
|
|
100
|
+
const xml = await toDeployableBpmn(wf, opts);
|
|
57
101
|
const form = new FormData();
|
|
58
102
|
form.append("resources", new Blob([xml], { type: "text/xml" }), `${wf.id}.bpmn`);
|
|
59
103
|
return this.json(`/v2/deployments`, { method: "POST", body: form }, "deploy");
|
package/dist/declarative.d.ts
CHANGED
|
@@ -33,12 +33,19 @@ export interface FlowBuilder<C extends FlowContracts = Record<string, never>> {
|
|
|
33
33
|
run<K extends string>(name: K, handler: TypedHandler<VarsOf<C, K>, ResultOf<C, K>>): FlowBuilder<C>;
|
|
34
34
|
/**
|
|
35
35
|
* A durable activity served by a worker OUTSIDE this program (a BPMN service
|
|
36
|
-
* task
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
36
|
+
* task, but no locally-hosted handler). Its job type defaults to the derived
|
|
37
|
+
* `${flowId}:${name}`; pass `{ jobType }` to override it with an explicit
|
|
38
|
+
* worker token (e.g. a `rank:capability` token like `senior:pr-review` that a
|
|
39
|
+
* `c8ctl nano work` matrix subscribes to) so an existing pool of agents can
|
|
40
|
+
* service it without renaming the flow. The step name stays the BPMN element
|
|
41
|
+
* id; only the emitted `zeebe:taskDefinition` type changes. Use
|
|
42
|
+
* `externalJobTypes(flow)` to list the (possibly overridden) types those
|
|
43
|
+
* workers must poll. Its contract envelopes (if any) type the model, not a
|
|
44
|
+
* local handler.
|
|
40
45
|
*/
|
|
41
|
-
task<K extends string>(name: K
|
|
46
|
+
task<K extends string>(name: K, opts?: {
|
|
47
|
+
jobType?: string;
|
|
48
|
+
}): FlowBuilder<C>;
|
|
42
49
|
/**
|
|
43
50
|
* A durable wait for an external/human event, correlated on a process
|
|
44
51
|
* variable (a BPMN message intermediate catch event). Resume it with
|
|
@@ -89,8 +96,11 @@ export declare function defineFlow(id: string, build: (w: FlowBuilder) => void):
|
|
|
89
96
|
/** Depth-first visit of every node in a flow tree (structural combinators
|
|
90
97
|
* recurse into their bodies). */
|
|
91
98
|
export declare function walkNodes(nodes: FlowNode[], visit: (n: FlowNode) => void): void;
|
|
92
|
-
/** The
|
|
93
|
-
*
|
|
99
|
+
/** The job types of a flow's external `task` steps (anywhere in the tree) — the
|
|
100
|
+
* contract workers outside this program must subscribe to. Each is the derived
|
|
101
|
+
* `<flowId>:<stepName>` unless the step overrode it via `w.task(name,
|
|
102
|
+
* { jobType })`. Deduplicated (preserving first-seen order) since several steps
|
|
103
|
+
* may intentionally share one override token. */
|
|
94
104
|
export declare function externalJobTypes(flow: DeclarativeFlow): string[];
|
|
95
105
|
/** Derive an executable BPMN model from a declarative flow. */
|
|
96
106
|
export declare function declarativeToBpmn(flow: DeclarativeFlow): string;
|
package/dist/declarative.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// Typed data envelopes are LIFTED into the model (nano:shape + dataEnvelope
|
|
35
35
|
// zeebe:property), so the generated .bpmn is ejectable to model-first with its
|
|
36
36
|
// typed contracts intact.
|
|
37
|
-
import { assertIdent, escapeXml, jobType, messageName } from "./xml.js";
|
|
37
|
+
import { assertIdent, assertJobType, escapeXml, jobType, messageName } from "./xml.js";
|
|
38
38
|
/** Ids the emitter generates for structural nodes / flows / messages. A step
|
|
39
39
|
* name that collides with one of these would produce a duplicate BPMN id and an
|
|
40
40
|
* invalid model, so reject them at authoring time. */
|
|
@@ -77,9 +77,12 @@ function makeBuilder(id, out, ctx) {
|
|
|
77
77
|
out.push({ kind: "run", name, envelopes: contractEnvelopes(ctx, name) });
|
|
78
78
|
return b;
|
|
79
79
|
},
|
|
80
|
-
task(name) {
|
|
80
|
+
task(name, opts) {
|
|
81
81
|
claimName(ctx, id, name);
|
|
82
|
-
|
|
82
|
+
const override = opts?.jobType;
|
|
83
|
+
if (override !== undefined)
|
|
84
|
+
assertJobType("task jobType", override);
|
|
85
|
+
out.push({ kind: "task", name, envelopes: contractEnvelopes(ctx, name), jobType: override });
|
|
83
86
|
return b;
|
|
84
87
|
},
|
|
85
88
|
signal(name, opts) {
|
|
@@ -181,13 +184,22 @@ export function walkNodes(nodes, visit) {
|
|
|
181
184
|
}
|
|
182
185
|
}
|
|
183
186
|
}
|
|
184
|
-
/** The
|
|
185
|
-
*
|
|
187
|
+
/** The job types of a flow's external `task` steps (anywhere in the tree) — the
|
|
188
|
+
* contract workers outside this program must subscribe to. Each is the derived
|
|
189
|
+
* `<flowId>:<stepName>` unless the step overrode it via `w.task(name,
|
|
190
|
+
* { jobType })`. Deduplicated (preserving first-seen order) since several steps
|
|
191
|
+
* may intentionally share one override token. */
|
|
186
192
|
export function externalJobTypes(flow) {
|
|
193
|
+
const seen = new Set();
|
|
187
194
|
const types = [];
|
|
188
195
|
walkNodes(flow.steps, (n) => {
|
|
189
|
-
if (n.kind
|
|
190
|
-
|
|
196
|
+
if (n.kind !== "task")
|
|
197
|
+
return;
|
|
198
|
+
const type = n.jobType ?? jobType(flow.id, n.name);
|
|
199
|
+
if (seen.has(type))
|
|
200
|
+
return;
|
|
201
|
+
seen.add(type);
|
|
202
|
+
types.push(type);
|
|
191
203
|
});
|
|
192
204
|
return types;
|
|
193
205
|
}
|
|
@@ -224,7 +236,7 @@ class Compiler {
|
|
|
224
236
|
this.envelopes.set(env.name, env.fields);
|
|
225
237
|
}
|
|
226
238
|
addServiceTask(node) {
|
|
227
|
-
const type = jobType(this.flow.id, node.name);
|
|
239
|
+
const type = node.jobType ?? jobType(this.flow.id, node.name);
|
|
228
240
|
this.recordEnvelope(node.envelopes?.in);
|
|
229
241
|
this.recordEnvelope(node.envelopes?.out);
|
|
230
242
|
const props = [];
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { FlowBuilder } from "./declarative.js";
|
|
|
5
5
|
export { layoutBpmn, declarativeToLayoutedBpmn } from "./layout.js";
|
|
6
6
|
export { envelope } from "./envelope.js";
|
|
7
7
|
export type { Envelope, EnvelopeType, EnvelopeField, FieldSpec, ScalarType, ScalarTs, FieldTs, } from "./envelope.js";
|
|
8
|
-
export { WorkflowClient, WorkflowError, toBpmn } from "./client.js";
|
|
8
|
+
export { WorkflowClient, WorkflowError, toBpmn, toDeployableBpmn } from "./client.js";
|
|
9
9
|
export type { WorkflowClientOptions, ActivateOptions } from "./client.js";
|
|
10
10
|
export { Worker } from "./worker.js";
|
|
11
11
|
export type { WorkerOptions, ActivityEvent } from "./worker.js";
|
package/dist/index.js
CHANGED
|
@@ -42,5 +42,5 @@ export { defineWorkflow, imperativeToBpmn, replayOnce } from "./imperative.js";
|
|
|
42
42
|
export { defineFlow, declarativeToBpmn, externalJobTypes, walkNodes } from "./declarative.js";
|
|
43
43
|
export { layoutBpmn, declarativeToLayoutedBpmn } from "./layout.js";
|
|
44
44
|
export { envelope } from "./envelope.js";
|
|
45
|
-
export { WorkflowClient, WorkflowError, toBpmn } from "./client.js";
|
|
45
|
+
export { WorkflowClient, WorkflowError, toBpmn, toDeployableBpmn } from "./client.js";
|
|
46
46
|
export { Worker } from "./worker.js";
|
package/dist/layout.js
CHANGED
|
@@ -43,8 +43,9 @@ export async function layoutBpmn(bpmnXml) {
|
|
|
43
43
|
"Install it to generate diagram layout: npm i bpmn-auto-layout", { cause });
|
|
44
44
|
}
|
|
45
45
|
const result = await layoutProcess(bpmnXml);
|
|
46
|
-
// Version-shape normalization: bpmn-auto-layout >= 1.4
|
|
47
|
-
// `{ xml, warnings }`; 1.3.x
|
|
46
|
+
// Version-shape normalization: bpmn-auto-layout 2.x (and >= 1.4) resolves to
|
|
47
|
+
// `{ xml, warnings }`; 1.3.x resolved to the laid-out XML string directly. We
|
|
48
|
+
// pin >= 2.0.0-alpha.2 but normalize both so the SDK tolerates the whole range.
|
|
48
49
|
return typeof result === "string" ? result : result.xml;
|
|
49
50
|
}
|
|
50
51
|
/**
|
package/dist/types.d.ts
CHANGED
package/dist/xml.d.ts
CHANGED
|
@@ -7,4 +7,5 @@ export declare const messageName: (workflowId: string, step: string) => string;
|
|
|
7
7
|
/** The single orchestrator job type of an imperative workflow. */
|
|
8
8
|
export declare const orchestrateType: (workflowId: string) => string;
|
|
9
9
|
export declare function assertIdent(kind: string, value: string): void;
|
|
10
|
+
export declare function assertJobType(kind: string, value: string): void;
|
|
10
11
|
export declare function assertWorkflowIds(wf: Workflow): void;
|
package/dist/xml.js
CHANGED
|
@@ -19,6 +19,16 @@ export function assertIdent(kind, value) {
|
|
|
19
19
|
throw new Error(`${kind} "${value}" is not a valid BPMN identifier (expected an NCName)`);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
/** An explicit job type override (e.g. a `rank:capability` worker token like
|
|
23
|
+
* `senior:pr-review`). Unlike a step name it is NOT a BPMN element id, so it may
|
|
24
|
+
* carry the `:`/`+` token delimiters a `c8ctl nano work` matrix uses; it only
|
|
25
|
+
* has to be a non-empty, whitespace-free token. */
|
|
26
|
+
const JOB_TYPE = /^[A-Za-z0-9_][A-Za-z0-9_.:+-]*$/;
|
|
27
|
+
export function assertJobType(kind, value) {
|
|
28
|
+
if (typeof value !== "string" || !JOB_TYPE.test(value)) {
|
|
29
|
+
throw new Error(`${kind} "${value}" is not a valid job type (expected a non-empty token of letters, digits, and _ . : + -)`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
22
32
|
export function assertWorkflowIds(wf) {
|
|
23
33
|
assertIdent("workflow id", wf.id);
|
|
24
34
|
if (wf.kind === "declarative") {
|
|
@@ -29,10 +39,14 @@ function assertNodeNames(nodes) {
|
|
|
29
39
|
for (const node of nodes) {
|
|
30
40
|
switch (node.kind) {
|
|
31
41
|
case "run":
|
|
32
|
-
case "task":
|
|
33
42
|
case "signal":
|
|
34
43
|
assertIdent("step name", node.name);
|
|
35
44
|
break;
|
|
45
|
+
case "task":
|
|
46
|
+
assertIdent("step name", node.name);
|
|
47
|
+
if (node.jobType !== undefined)
|
|
48
|
+
assertJobType("job type", node.jobType);
|
|
49
|
+
break;
|
|
36
50
|
case "switch":
|
|
37
51
|
for (const c of node.cases)
|
|
38
52
|
assertNodeNames(c.body);
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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 over the REST v2 API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/
|
|
10
|
-
"directory": "workflow"
|
|
9
|
+
"url": "https://github.com/jwulf/nano-ide.git",
|
|
10
|
+
"directory": "packages/workflow"
|
|
11
11
|
},
|
|
12
12
|
"main": "./dist/index.js",
|
|
13
13
|
"module": "./dist/index.js",
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^22.20.1",
|
|
40
|
-
"bpmn-auto-layout": "^
|
|
40
|
+
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
41
41
|
"typescript": "^5.6.3"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"bpmn-auto-layout": "^
|
|
44
|
+
"bpmn-auto-layout": "^2.0.0-alpha.2"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"bpmn-auto-layout": {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// Minimal ambient types for `bpmn-auto-layout` (
|
|
2
|
-
// adds diagram interchange (DI) to a BPMN XML string. The
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// site (see `layout.ts`).
|
|
1
|
+
// Minimal ambient types for `bpmn-auto-layout` (ships none of its own).
|
|
2
|
+
// `layoutProcess` adds diagram interchange (DI) to a BPMN XML string. The
|
|
3
|
+
// resolved shape has settled on `{ xml, warnings }` in 2.x (and >= 1.4); 1.3.x
|
|
4
|
+
// resolved to the laid-out XML string directly. We type the union and normalize
|
|
5
|
+
// at the call site (see `layout.ts`) so the SDK still works across that range.
|
|
6
6
|
declare module "bpmn-auto-layout" {
|
|
7
7
|
export function layoutProcess(
|
|
8
8
|
xml: string,
|
package/src/client.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { declarativeToBpmn, walkNodes } from "./declarative.js";
|
|
6
6
|
import { imperativeToBpmn } from "./imperative.js";
|
|
7
|
+
import { layoutBpmn } from "./layout.js";
|
|
7
8
|
import type { DeclarativeFlow, DeployResult, Job, JsonObject, StartResult, Workflow } from "./types.js";
|
|
8
9
|
import { assertWorkflowIds, messageName } from "./xml.js";
|
|
9
10
|
|
|
@@ -13,6 +14,47 @@ export function toBpmn(wf: Workflow): string {
|
|
|
13
14
|
return wf.kind === "imperative" ? imperativeToBpmn(wf) : declarativeToBpmn(wf);
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
let warnedNoLayout = false;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render a workflow to *deployable* BPMN — the executable model plus diagram
|
|
21
|
+
* interchange (DI), auto-generated with `bpmn-auto-layout` — so the deployed
|
|
22
|
+
* process opens rendered and inspectable in a modeller/Operate rather than as a
|
|
23
|
+
* blank canvas. The semantic model stays authoritative; DI is derived.
|
|
24
|
+
*
|
|
25
|
+
* DI generation needs the optional peer dependency `bpmn-auto-layout`. When it is
|
|
26
|
+
* absent, this degrades gracefully: it warns once and returns the DI-less model
|
|
27
|
+
* so `deploy` still works everywhere. Pass `{ layout: false }` to skip layout
|
|
28
|
+
* deliberately. A genuine layout failure (the dep is installed but errors) is
|
|
29
|
+
* surfaced, not swallowed.
|
|
30
|
+
*/
|
|
31
|
+
export async function toDeployableBpmn(
|
|
32
|
+
wf: Workflow,
|
|
33
|
+
opts: { layout?: boolean } = {},
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
const semantic = toBpmn(wf);
|
|
36
|
+
if (opts.layout === false) return semantic;
|
|
37
|
+
try {
|
|
38
|
+
return await layoutBpmn(semantic);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// Only fall back for the "optional dep not installed" case (layoutBpmn wraps
|
|
41
|
+
// it with an ERR_MODULE_NOT_FOUND cause). Any other failure is a real layout
|
|
42
|
+
// problem and must surface rather than silently deploy an uninspectable model.
|
|
43
|
+
const cause = (e as { cause?: NodeJS.ErrnoException })?.cause;
|
|
44
|
+
if (cause?.code !== "ERR_MODULE_NOT_FOUND") throw e;
|
|
45
|
+
if (!warnedNoLayout) {
|
|
46
|
+
warnedNoLayout = true;
|
|
47
|
+
console.warn(
|
|
48
|
+
`[@nanobpm/workflow] Deploying "${wf.id}" without a diagram: the optional ` +
|
|
49
|
+
'"bpmn-auto-layout" dependency is not installed, so the model will be ' +
|
|
50
|
+
"uninspectable in a modeller/Operate. Install it for diagram layout: " +
|
|
51
|
+
"npm i bpmn-auto-layout",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return semantic;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
16
58
|
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
17
59
|
|
|
18
60
|
export interface WorkflowClientOptions {
|
|
@@ -75,9 +117,15 @@ export class WorkflowClient {
|
|
|
75
117
|
return res;
|
|
76
118
|
}
|
|
77
119
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Deploy a workflow's derived BPMN model. The deployed model includes
|
|
122
|
+
* auto-generated diagram interchange (DI) so it is inspectable in a
|
|
123
|
+
* modeller/Operate; pass `{ layout: false }` to deploy the DI-less semantic
|
|
124
|
+
* model. DI needs the optional `bpmn-auto-layout` dependency — see
|
|
125
|
+
* `toDeployableBpmn` for the graceful-degradation behaviour when it is absent.
|
|
126
|
+
*/
|
|
127
|
+
async deploy(wf: Workflow, opts: { layout?: boolean } = {}): Promise<DeployResult> {
|
|
128
|
+
const xml = await toDeployableBpmn(wf, opts);
|
|
81
129
|
const form = new FormData();
|
|
82
130
|
form.append("resources", new Blob([xml], { type: "text/xml" }), `${wf.id}.bpmn`);
|
|
83
131
|
return this.json<DeployResult>(`/v2/deployments`, { method: "POST", body: form }, "deploy");
|
package/src/declarative.ts
CHANGED
|
@@ -46,7 +46,7 @@ import type {
|
|
|
46
46
|
SwitchCase,
|
|
47
47
|
} from "./types.js";
|
|
48
48
|
import type { Envelope, EnvelopeField } from "./envelope.js";
|
|
49
|
-
import { assertIdent, escapeXml, jobType, messageName } from "./xml.js";
|
|
49
|
+
import { assertIdent, assertJobType, escapeXml, jobType, messageName } from "./xml.js";
|
|
50
50
|
|
|
51
51
|
// --- Authoring surface -------------------------------------------------------
|
|
52
52
|
|
|
@@ -82,12 +82,17 @@ export interface FlowBuilder<C extends FlowContracts = Record<string, never>> {
|
|
|
82
82
|
run<K extends string>(name: K, handler: TypedHandler<VarsOf<C, K>, ResultOf<C, K>>): FlowBuilder<C>;
|
|
83
83
|
/**
|
|
84
84
|
* A durable activity served by a worker OUTSIDE this program (a BPMN service
|
|
85
|
-
* task
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
85
|
+
* task, but no locally-hosted handler). Its job type defaults to the derived
|
|
86
|
+
* `${flowId}:${name}`; pass `{ jobType }` to override it with an explicit
|
|
87
|
+
* worker token (e.g. a `rank:capability` token like `senior:pr-review` that a
|
|
88
|
+
* `c8ctl nano work` matrix subscribes to) so an existing pool of agents can
|
|
89
|
+
* service it without renaming the flow. The step name stays the BPMN element
|
|
90
|
+
* id; only the emitted `zeebe:taskDefinition` type changes. Use
|
|
91
|
+
* `externalJobTypes(flow)` to list the (possibly overridden) types those
|
|
92
|
+
* workers must poll. Its contract envelopes (if any) type the model, not a
|
|
93
|
+
* local handler.
|
|
89
94
|
*/
|
|
90
|
-
task<K extends string>(name: K): FlowBuilder<C>;
|
|
95
|
+
task<K extends string>(name: K, opts?: { jobType?: string }): FlowBuilder<C>;
|
|
91
96
|
/**
|
|
92
97
|
* A durable wait for an external/human event, correlated on a process
|
|
93
98
|
* variable (a BPMN message intermediate catch event). Resume it with
|
|
@@ -173,9 +178,11 @@ function makeBuilder<C extends FlowContracts>(id: string, out: FlowNode[], ctx:
|
|
|
173
178
|
out.push({ kind: "run", name, envelopes: contractEnvelopes(ctx, name) });
|
|
174
179
|
return b as unknown as FlowBuilder<C>;
|
|
175
180
|
},
|
|
176
|
-
task(name: string): FlowBuilder<C> {
|
|
181
|
+
task(name: string, opts?: { jobType?: string }): FlowBuilder<C> {
|
|
177
182
|
claimName(ctx, id, name);
|
|
178
|
-
|
|
183
|
+
const override = opts?.jobType;
|
|
184
|
+
if (override !== undefined) assertJobType("task jobType", override);
|
|
185
|
+
out.push({ kind: "task", name, envelopes: contractEnvelopes(ctx, name), jobType: override });
|
|
179
186
|
return b as unknown as FlowBuilder<C>;
|
|
180
187
|
},
|
|
181
188
|
signal(name: string, opts: { correlationKey: string }): FlowBuilder<C> {
|
|
@@ -286,12 +293,20 @@ export function walkNodes(nodes: FlowNode[], visit: (n: FlowNode) => void): void
|
|
|
286
293
|
}
|
|
287
294
|
}
|
|
288
295
|
|
|
289
|
-
/** The
|
|
290
|
-
*
|
|
296
|
+
/** The job types of a flow's external `task` steps (anywhere in the tree) — the
|
|
297
|
+
* contract workers outside this program must subscribe to. Each is the derived
|
|
298
|
+
* `<flowId>:<stepName>` unless the step overrode it via `w.task(name,
|
|
299
|
+
* { jobType })`. Deduplicated (preserving first-seen order) since several steps
|
|
300
|
+
* may intentionally share one override token. */
|
|
291
301
|
export function externalJobTypes(flow: DeclarativeFlow): string[] {
|
|
302
|
+
const seen = new Set<string>();
|
|
292
303
|
const types: string[] = [];
|
|
293
304
|
walkNodes(flow.steps, (n) => {
|
|
294
|
-
if (n.kind
|
|
305
|
+
if (n.kind !== "task") return;
|
|
306
|
+
const type = n.jobType ?? jobType(flow.id, n.name);
|
|
307
|
+
if (seen.has(type)) return;
|
|
308
|
+
seen.add(type);
|
|
309
|
+
types.push(type);
|
|
295
310
|
});
|
|
296
311
|
return types;
|
|
297
312
|
}
|
|
@@ -352,8 +367,8 @@ class Compiler {
|
|
|
352
367
|
this.envelopes.set(env.name, env.fields);
|
|
353
368
|
}
|
|
354
369
|
|
|
355
|
-
private addServiceTask(node: { name: string; envelopes?: NodeEnvelopes }): void {
|
|
356
|
-
const type = jobType(this.flow.id, node.name);
|
|
370
|
+
private addServiceTask(node: { name: string; envelopes?: NodeEnvelopes; jobType?: string }): void {
|
|
371
|
+
const type = node.jobType ?? jobType(this.flow.id, node.name);
|
|
357
372
|
this.recordEnvelope(node.envelopes?.in);
|
|
358
373
|
this.recordEnvelope(node.envelopes?.out);
|
|
359
374
|
const props: string[] = [];
|
package/src/index.ts
CHANGED
|
@@ -54,7 +54,7 @@ export type {
|
|
|
54
54
|
ScalarTs,
|
|
55
55
|
FieldTs,
|
|
56
56
|
} from "./envelope.js";
|
|
57
|
-
export { WorkflowClient, WorkflowError, toBpmn } from "./client.js";
|
|
57
|
+
export { WorkflowClient, WorkflowError, toBpmn, toDeployableBpmn } from "./client.js";
|
|
58
58
|
export type { WorkflowClientOptions, ActivateOptions } from "./client.js";
|
|
59
59
|
export { Worker } from "./worker.js";
|
|
60
60
|
export type { WorkerOptions, ActivityEvent } from "./worker.js";
|
package/src/layout.ts
CHANGED
|
@@ -48,8 +48,9 @@ export async function layoutBpmn(bpmnXml: string): Promise<string> {
|
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
const result = await layoutProcess(bpmnXml);
|
|
51
|
-
// Version-shape normalization: bpmn-auto-layout >= 1.4
|
|
52
|
-
// `{ xml, warnings }`; 1.3.x
|
|
51
|
+
// Version-shape normalization: bpmn-auto-layout 2.x (and >= 1.4) resolves to
|
|
52
|
+
// `{ xml, warnings }`; 1.3.x resolved to the laid-out XML string directly. We
|
|
53
|
+
// pin >= 2.0.0-alpha.2 but normalize both so the SDK tolerates the whole range.
|
|
53
54
|
return typeof result === "string" ? result : result.xml;
|
|
54
55
|
}
|
|
55
56
|
|
package/src/types.ts
CHANGED
|
@@ -51,7 +51,7 @@ export type StepHandler = (job: Job) => Promise<JsonObject | void> | JsonObject
|
|
|
51
51
|
* combinators carry nested `FlowNode[]` bodies. */
|
|
52
52
|
export type FlowNode =
|
|
53
53
|
| { kind: "run"; name: string; envelopes?: NodeEnvelopes }
|
|
54
|
-
| { kind: "task"; name: string; envelopes?: NodeEnvelopes }
|
|
54
|
+
| { kind: "task"; name: string; envelopes?: NodeEnvelopes; jobType?: string }
|
|
55
55
|
| { kind: "signal"; name: string; correlationKey: string; payload?: Envelope }
|
|
56
56
|
| { kind: "switch"; subject: string; cases: SwitchCase[]; default?: FlowNode[] }
|
|
57
57
|
| { kind: "branch"; condition: string; then: FlowNode[]; else?: FlowNode[] }
|
package/src/xml.ts
CHANGED
|
@@ -27,6 +27,19 @@ export function assertIdent(kind: string, value: string): void {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** An explicit job type override (e.g. a `rank:capability` worker token like
|
|
31
|
+
* `senior:pr-review`). Unlike a step name it is NOT a BPMN element id, so it may
|
|
32
|
+
* carry the `:`/`+` token delimiters a `c8ctl nano work` matrix uses; it only
|
|
33
|
+
* has to be a non-empty, whitespace-free token. */
|
|
34
|
+
const JOB_TYPE = /^[A-Za-z0-9_][A-Za-z0-9_.:+-]*$/;
|
|
35
|
+
export function assertJobType(kind: string, value: string): void {
|
|
36
|
+
if (typeof value !== "string" || !JOB_TYPE.test(value)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`${kind} "${value}" is not a valid job type (expected a non-empty token of letters, digits, and _ . : + -)`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
export function assertWorkflowIds(wf: Workflow): void {
|
|
31
44
|
assertIdent("workflow id", wf.id);
|
|
32
45
|
if (wf.kind === "declarative") {
|
|
@@ -38,10 +51,13 @@ function assertNodeNames(nodes: FlowNode[]): void {
|
|
|
38
51
|
for (const node of nodes) {
|
|
39
52
|
switch (node.kind) {
|
|
40
53
|
case "run":
|
|
41
|
-
case "task":
|
|
42
54
|
case "signal":
|
|
43
55
|
assertIdent("step name", node.name);
|
|
44
56
|
break;
|
|
57
|
+
case "task":
|
|
58
|
+
assertIdent("step name", node.name);
|
|
59
|
+
if (node.jobType !== undefined) assertJobType("job type", node.jobType);
|
|
60
|
+
break;
|
|
45
61
|
case "switch":
|
|
46
62
|
for (const c of node.cases) assertNodeNames(c.body);
|
|
47
63
|
if (node.default) assertNodeNames(node.default);
|