@nanobpm/workflow 0.1.0 → 0.3.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.
@@ -18,7 +18,13 @@
18
18
  // The orchestration body MUST be deterministic across replays (no wall-clock
19
19
  // branching, RNG, or I/O outside `ctx.run` handlers).
20
20
  import { assertIdent, escapeXml, orchestrateType } from "./xml.js";
21
- /** Define an imperative, replay-driven durable workflow. */
21
+ /** Define an imperative, replay-driven durable workflow.
22
+ *
23
+ * @experimental Not the recommended code-first surface. Prefer `defineFlow`
24
+ * (declarative), whose steps are engine-visible BPMN nodes. This imperative
25
+ * replay surface compiles to a single opaque looping orchestrator and requires
26
+ * determinism discipline in the orchestration body; it is retained for advanced
27
+ * durable-orchestration use only. */
22
28
  export function defineWorkflow(id, orchestrate) {
23
29
  assertIdent("workflow id", id);
24
30
  if (typeof orchestrate !== "function")
package/dist/index.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  export { defineWorkflow, imperativeToBpmn, replayOnce } from "./imperative.js";
2
2
  export type { Journal, ReplayStep } from "./imperative.js";
3
- export { defineFlow, declarativeToBpmn } from "./declarative.js";
3
+ export { defineFlow, declarativeToBpmn, externalJobTypes, walkNodes } from "./declarative.js";
4
4
  export type { FlowBuilder } from "./declarative.js";
5
+ export { layoutBpmn, declarativeToLayoutedBpmn } from "./layout.js";
6
+ export { envelope } from "./envelope.js";
7
+ export type { Envelope, EnvelopeType, EnvelopeField, FieldSpec, ScalarType, ScalarTs, FieldTs, } from "./envelope.js";
5
8
  export { WorkflowClient, WorkflowError, toBpmn } from "./client.js";
6
9
  export type { WorkflowClientOptions, ActivateOptions } from "./client.js";
7
10
  export { Worker } from "./worker.js";
8
11
  export type { WorkerOptions, ActivityEvent } from "./worker.js";
9
- export type { Json, JsonObject, Job, StepHandler, DeclarativeFlow, DeclarativeStep, ImperativeWorkflow, Orchestration, WorkflowContext, Workflow, DeployResult, StartResult, } from "./types.js";
12
+ export type { Json, JsonObject, Job, StepHandler, DeclarativeFlow, DeclarativeStep, FlowNode, SwitchCase, NodeEnvelopes, StepContract, FlowContracts, ImperativeWorkflow, Orchestration, WorkflowContext, Workflow, DeployResult, StartResult, } from "./types.js";
package/dist/index.js CHANGED
@@ -1,28 +1,46 @@
1
- // @nanobpm/workflow — code-first durable orchestration for nanobpmn (ADR 0044).
1
+ // @nanobpm/workflow — code-first durable workflows for nanobpmn (ADR 0044/0045).
2
2
  //
3
- // Two authoring surfaces over the same engine durability:
3
+ // THE code-first surface is the declarative flow builder — a list of steps the
4
+ // engine runs and shows you one at a time, because each compiles to a real,
5
+ // engine-visible BPMN node:
4
6
  //
5
- // defineWorkflow(id, async (ctx) => { await ctx.run(...) }) // imperative (replay)
6
- // defineFlow(id, (w) => { w.run(...); w.signal(...); }) // declarative (+signals)
7
+ // defineFlow(id, (w) => {
8
+ // w.run("fetchDiff", async (job) => ({ diff: await gh.diff(job.variables.prId) }));
9
+ // w.signal("humanApproval", { correlationKey: "prId" }); // durable human wait
10
+ // w.task("signPdf"); // served by an EXTERNAL worker
11
+ // w.run("merge", async (job) => ({ merged: await gh.merge(job.variables.prId) }));
12
+ // });
13
+ //
14
+ // The SDK derives the BPMN model, the job types (`${flowId}:${step}`), and the
15
+ // message/correlation wiring; the Worker hosts your `run` steps; the
16
+ // WorkflowClient deploys, starts, and signals. `w.task` steps are served by a
17
+ // worker outside this program (see `externalJobTypes`).
7
18
  //
8
- // The SDK derives the BPMN model, job types, and message/correlation wiring; the
9
- // Worker runtime hosts them; the WorkflowClient deploys, starts, and signals.
19
+ // `defineWorkflow` (imperative, Temporal-style replay) is EXPERIMENTAL and kept
20
+ // for advanced durable-orchestration use only; prefer `defineFlow`. Its steps
21
+ // are not engine-visible (a single looping orchestrator) and it requires
22
+ // determinism discipline. It is not the recommended authoring surface.
10
23
  //
11
24
  // Quickstart:
12
25
  //
13
- // import { defineWorkflow, WorkflowClient, Worker } from "@nanobpm/workflow";
26
+ // import { defineFlow, WorkflowClient, Worker } from "@nanobpm/workflow";
14
27
  //
15
- // const wf = defineWorkflow("pr-review", async (ctx) => {
16
- // const diff = await ctx.run("fetchDiff", () => gh.diff(ctx.input.prId));
17
- // await ctx.run("merge", () => gh.merge(ctx.input.prId));
28
+ // const prReview = defineFlow("pr-review", (w) => {
29
+ // w.run("fetchDiff", async (job) => ({ files: 3 }));
30
+ // w.run("merge", async (job) => ({ merged: true }));
18
31
  // });
19
32
  //
20
33
  // const client = new WorkflowClient({ baseUrl: "http://localhost:8080" });
21
- // await client.deploy(wf);
22
- // const worker = new Worker({ baseUrl: "http://localhost:8080", workflows: [wf] });
34
+ // await client.deploy(prReview);
35
+ // const worker = new Worker({ baseUrl: "http://localhost:8080", workflows: [prReview] });
23
36
  // worker.start();
24
- // await client.start(wf, { prId: "PR-1234" });
37
+ // await client.start(prReview, { prId: "PR-1234" });
38
+ // EXPERIMENTAL — imperative (Temporal-style replay) surface. Not the recommended
39
+ // code-first surface (steps are not engine-visible; requires determinism
40
+ // discipline). Retained for advanced durable-orchestration use only.
25
41
  export { defineWorkflow, imperativeToBpmn, replayOnce } from "./imperative.js";
26
- export { defineFlow, declarativeToBpmn } from "./declarative.js";
42
+ export { defineFlow, declarativeToBpmn, externalJobTypes, walkNodes } from "./declarative.js";
43
+ export { layoutBpmn, declarativeToLayoutedBpmn } from "./layout.js";
44
+ export { envelope } from "./envelope.js";
27
45
  export { WorkflowClient, WorkflowError, toBpmn } from "./client.js";
28
46
  export { Worker } from "./worker.js";
@@ -0,0 +1,22 @@
1
+ import type { DeclarativeFlow } from "./types.js";
2
+ /**
3
+ * Generate BPMN diagram interchange (DI) for a BPMN XML string using bpmn-io's
4
+ * `bpmn-auto-layout`, returning the same model with an auto-laid-out diagram
5
+ * (`bpmndi:BPMNDiagram`). Works on DI-less *or* already-laid-out input, and
6
+ * preserves `zeebe:` extension elements (task definitions, message
7
+ * subscriptions) through the round-trip.
8
+ *
9
+ * Requires the optional peer dependency `bpmn-auto-layout` to be installed; a
10
+ * clear error is thrown if it is missing.
11
+ */
12
+ export declare function layoutBpmn(bpmnXml: string): Promise<string>;
13
+ /**
14
+ * Derive an executable BPMN model from a declarative flow AND auto-generate its
15
+ * diagram (DI), so the model opens rendered in a modeller/viewer. Convenience
16
+ * over `layoutBpmn(declarativeToBpmn(flow))`; async because layout is async. The
17
+ * semantic model stays authoritative — see `declarativeToBpmn` for the DI-less
18
+ * model the engine actually runs.
19
+ *
20
+ * Requires the optional peer dependency `bpmn-auto-layout` to be installed.
21
+ */
22
+ export declare function declarativeToLayoutedBpmn(flow: DeclarativeFlow): Promise<string>;
package/dist/layout.js ADDED
@@ -0,0 +1,61 @@
1
+ // Optional diagram-interchange (DI) generation for the code-first surfaces.
2
+ //
3
+ // The authoring surfaces emit *semantic* BPMN — process, flow nodes, sequence
4
+ // flows, messages — with no `bpmndi:` diagram: the engine runs the semantic model
5
+ // and DI is an authoring-time concern only (a derived flow has no diagram of its
6
+ // own). This module layers bpmn-io's `bpmn-auto-layout` on top so a derived model
7
+ // opens with a rendered, auto-laid-out diagram in a modeller/viewer, which makes
8
+ // code-first flows visually inspectable, reviewable, and round-trippable.
9
+ //
10
+ // `bpmn-auto-layout` is an *optional peer dependency*: the runtime client stays
11
+ // dependency-free and the layout engine is loaded lazily, only when you ask for a
12
+ // diagram. Install it alongside this package (`npm i bpmn-auto-layout`) to use
13
+ // these helpers.
14
+ //
15
+ // DI is best-effort and *regenerable* — the semantic model (`declarativeToBpmn`)
16
+ // stays authoritative. Re-run layout whenever the flow changes rather than
17
+ // hand-editing the generated diagram.
18
+ import { declarativeToBpmn } from "./declarative.js";
19
+ /**
20
+ * Generate BPMN diagram interchange (DI) for a BPMN XML string using bpmn-io's
21
+ * `bpmn-auto-layout`, returning the same model with an auto-laid-out diagram
22
+ * (`bpmndi:BPMNDiagram`). Works on DI-less *or* already-laid-out input, and
23
+ * preserves `zeebe:` extension elements (task definitions, message
24
+ * subscriptions) through the round-trip.
25
+ *
26
+ * Requires the optional peer dependency `bpmn-auto-layout` to be installed; a
27
+ * clear error is thrown if it is missing.
28
+ */
29
+ export async function layoutBpmn(bpmnXml) {
30
+ let layoutProcess;
31
+ try {
32
+ ({ layoutProcess } = await import("bpmn-auto-layout"));
33
+ }
34
+ catch (cause) {
35
+ // Only substitute the friendly "not installed" message when the module
36
+ // genuinely cannot be resolved. A different failure (the package is present
37
+ // but fails to load — syntax, a broken transitive dep, a runtime throw at
38
+ // import time) must surface as-is so it isn't masked as a missing dependency.
39
+ if (cause?.code !== "ERR_MODULE_NOT_FOUND") {
40
+ throw cause;
41
+ }
42
+ throw new Error('layoutBpmn requires the optional peer dependency "bpmn-auto-layout". ' +
43
+ "Install it to generate diagram layout: npm i bpmn-auto-layout", { cause });
44
+ }
45
+ const result = await layoutProcess(bpmnXml);
46
+ // Version-shape normalization: bpmn-auto-layout >= 1.4 (and `main`) resolves to
47
+ // `{ xml, warnings }`; 1.3.x resolves to the laid-out XML string directly.
48
+ return typeof result === "string" ? result : result.xml;
49
+ }
50
+ /**
51
+ * Derive an executable BPMN model from a declarative flow AND auto-generate its
52
+ * diagram (DI), so the model opens rendered in a modeller/viewer. Convenience
53
+ * over `layoutBpmn(declarativeToBpmn(flow))`; async because layout is async. The
54
+ * semantic model stays authoritative — see `declarativeToBpmn` for the DI-less
55
+ * model the engine actually runs.
56
+ *
57
+ * Requires the optional peer dependency `bpmn-auto-layout` to be installed.
58
+ */
59
+ export function declarativeToLayoutedBpmn(flow) {
60
+ return layoutBpmn(declarativeToBpmn(flow));
61
+ }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Envelope } from "./envelope.js";
1
2
  /** A JSON-serialisable value, as carried by process variables. */
2
3
  export type Json = null | boolean | number | string | Json[] | {
3
4
  [k: string]: Json;
@@ -5,28 +6,82 @@ export type Json = null | boolean | number | string | Json[] | {
5
6
  export type JsonObject = {
6
7
  [k: string]: Json;
7
8
  };
8
- /** A job as delivered by the nanobpmn gateway's `POST /v2/jobs/activation`. */
9
- export interface Job {
9
+ /** A job as delivered by the nanobpmn gateway's `POST /v2/jobs/activation`. The
10
+ * `variables` type parameter carries the input envelope's inferred payload when
11
+ * a step is declared with a typed envelope; it defaults to the untyped
12
+ * `JsonObject`. */
13
+ export interface Job<V extends JsonObject = JsonObject> {
10
14
  jobKey: string;
11
15
  processInstanceKey: string;
12
16
  elementId: string;
13
17
  type: string;
14
- variables: JsonObject;
18
+ variables: V;
15
19
  }
20
+ /** The input/output data envelopes lifted onto an activity node. */
21
+ export interface NodeEnvelopes {
22
+ in?: Envelope;
23
+ out?: Envelope;
24
+ }
25
+ /** A typed I/O contract for one step: its input and/or output data envelope.
26
+ * For a `signal`, `in` types the message payload. */
27
+ export interface StepContract {
28
+ in?: Envelope;
29
+ out?: Envelope;
30
+ }
31
+ /** A flow's typed I/O registry, keyed by step name — the single source of truth
32
+ * for each step's envelopes. Passed to `defineFlow`; a step whose name is a key
33
+ * is typed (and its envelopes lifted to the model), others fall back to the
34
+ * untyped `JsonObject`. */
35
+ export type FlowContracts = Record<string, StepContract>;
16
36
  /** Handler for a declarative `run` step: does real work, returns variables. */
17
37
  export type StepHandler = (job: Job) => Promise<JsonObject | void> | JsonObject | void;
18
- export type DeclarativeStep = {
38
+ /** A node in a declarative flow tree. Leaf activities carry optional data
39
+ * envelopes (lifted to `nano:shape` + `dataEnvelope` in the model); structural
40
+ * combinators carry nested `FlowNode[]` bodies. */
41
+ export type FlowNode = {
19
42
  kind: "run";
20
43
  name: string;
44
+ envelopes?: NodeEnvelopes;
45
+ } | {
46
+ kind: "task";
47
+ name: string;
48
+ envelopes?: NodeEnvelopes;
21
49
  } | {
22
50
  kind: "signal";
23
51
  name: string;
24
52
  correlationKey: string;
53
+ payload?: Envelope;
54
+ } | {
55
+ kind: "switch";
56
+ subject: string;
57
+ cases: SwitchCase[];
58
+ default?: FlowNode[];
59
+ } | {
60
+ kind: "branch";
61
+ condition: string;
62
+ then: FlowNode[];
63
+ else?: FlowNode[];
64
+ } | {
65
+ kind: "loop";
66
+ body: FlowNode[];
67
+ } | {
68
+ kind: "break";
69
+ } | {
70
+ kind: "continue";
25
71
  };
72
+ /** One case of a `switch`: routed when `subject = value` (FEEL equality). */
73
+ export interface SwitchCase {
74
+ value: string;
75
+ body: FlowNode[];
76
+ }
77
+ /** @deprecated Renamed to {@link FlowNode} now that a flow is a tree of nodes,
78
+ * not a flat list of steps. Kept as an alias for source compatibility. */
79
+ export type DeclarativeStep = FlowNode;
26
80
  export interface DeclarativeFlow {
27
81
  kind: "declarative";
28
82
  id: string;
29
- steps: DeclarativeStep[];
83
+ /** The flow's node tree (top-level sequence). */
84
+ steps: FlowNode[];
30
85
  handlers: Record<string, StepHandler>;
31
86
  }
32
87
  /** The context passed to an imperative orchestration function. */
package/dist/worker.d.ts CHANGED
@@ -41,6 +41,18 @@ export declare class Worker {
41
41
  private loops;
42
42
  constructor(opts: WorkerOptions);
43
43
  private register;
44
+ /** Register a derived job type, failing fast on a collision. Two workflows can
45
+ * resolve to the same job type (duplicate workflow ids, or a declarative step
46
+ * name that collides with another workflow's); silently overwriting the route
47
+ * would drop a handler, so we reject it at construction time. */
48
+ private addRoute;
49
+ /** Invoke the onError observer hook without letting it affect the poll loop —
50
+ * a throwing observer must not permanently stop a route. */
51
+ private emitError;
52
+ /** Invoke the onActivity observer hook in isolation. It runs after the job has
53
+ * already been completed, so a throwing observer must not fall into the
54
+ * failure path and try to fail an already-completed job. */
55
+ private emitActivity;
44
56
  /** The derived job types this worker serves. */
45
57
  get servedTypes(): string[];
46
58
  /** Begin polling. Resolves once the loops are running (they run until stop()). */
package/dist/worker.js CHANGED
@@ -3,7 +3,8 @@
3
3
  // them. It generalises the two surfaces:
4
4
  //
5
5
  // - a declarative flow contributes one job type per `run` step, dispatched to
6
- // the user handler;
6
+ // the user handler (external `task` steps are intentionally NOT hosted here —
7
+ // a worker outside this program serves them);
7
8
  // - an imperative workflow contributes its single orchestrator job type,
8
9
  // dispatched to the replay engine (which advances the journal one step).
9
10
  //
@@ -12,6 +13,7 @@
12
13
  // the ADR 0044 spike proved.
13
14
  import { WorkflowClient } from "./client.js";
14
15
  import { replayOnce } from "./imperative.js";
16
+ import { walkNodes } from "./declarative.js";
15
17
  import { jobType } from "./xml.js";
16
18
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
17
19
  export class Worker {
@@ -39,7 +41,7 @@ export class Worker {
39
41
  }
40
42
  register(wf) {
41
43
  if (wf.kind === "imperative") {
42
- this.routes.set(wf.orchestrateType, {
44
+ this.addRoute(wf.orchestrateType, {
43
45
  workflowId: wf.id,
44
46
  handle: async (job) => {
45
47
  const input = job.variables.input ?? {};
@@ -53,15 +55,59 @@ export class Worker {
53
55
  });
54
56
  }
55
57
  else {
56
- for (const s of wf.steps) {
58
+ // Walk the flow tree; only `run` steps are hosted locally. `signal`
59
+ // (message catch) and `task` (external worker) contribute no in-process
60
+ // route, and structural combinators (`switch`/`branch`/`loop`) are pure
61
+ // routing with no job of their own.
62
+ walkNodes(wf.steps, (s) => {
57
63
  if (s.kind !== "run")
58
- continue;
64
+ return;
59
65
  const handler = wf.handlers[s.name];
60
- this.routes.set(jobType(wf.id, s.name), {
66
+ if (typeof handler !== "function") {
67
+ // `DeclarativeFlow` is a public type; a consumer-constructed flow could
68
+ // carry a `run` step with no handler. Fail fast at registration with a
69
+ // clear message rather than a `handler is not a function` TypeError on
70
+ // the first job activation.
71
+ throw new Error(`workflow "${wf.id}": run step "${s.name}" has no handler function`);
72
+ }
73
+ this.addRoute(jobType(wf.id, s.name), {
61
74
  workflowId: wf.id,
62
75
  handle: async (job) => ({ variables: ((await handler(job)) ?? {}) }),
63
76
  });
64
- }
77
+ });
78
+ }
79
+ }
80
+ /** Register a derived job type, failing fast on a collision. Two workflows can
81
+ * resolve to the same job type (duplicate workflow ids, or a declarative step
82
+ * name that collides with another workflow's); silently overwriting the route
83
+ * would drop a handler, so we reject it at construction time. */
84
+ addRoute(type, route) {
85
+ const existing = this.routes.get(type);
86
+ if (existing) {
87
+ throw new Error(`duplicate derived job type "${type}": workflows "${existing.workflowId}" and "${route.workflowId}" ` +
88
+ `resolve to the same job type (check for duplicate workflow ids or colliding step names)`);
89
+ }
90
+ this.routes.set(type, route);
91
+ }
92
+ /** Invoke the onError observer hook without letting it affect the poll loop —
93
+ * a throwing observer must not permanently stop a route. */
94
+ emitError(err, type) {
95
+ try {
96
+ this.onError?.(err, { type });
97
+ }
98
+ catch {
99
+ /* observer hooks are purely observational; swallow their failures */
100
+ }
101
+ }
102
+ /** Invoke the onActivity observer hook in isolation. It runs after the job has
103
+ * already been completed, so a throwing observer must not fall into the
104
+ * failure path and try to fail an already-completed job. */
105
+ async emitActivity(e) {
106
+ try {
107
+ await this.onActivity?.(e);
108
+ }
109
+ catch {
110
+ /* observational only; swallow so it never triggers failJob */
65
111
  }
66
112
  }
67
113
  /** The derived job types this worker serves. */
@@ -94,7 +140,7 @@ export class Worker {
94
140
  }
95
141
  catch (e) {
96
142
  // Transport/gateway error (e.g. engine restarting): back off, reconnect.
97
- this.onError?.(e, { type });
143
+ this.emitError(e, type);
98
144
  await sleep(this.backoffMs);
99
145
  continue;
100
146
  }
@@ -104,7 +150,9 @@ export class Worker {
104
150
  try {
105
151
  const { variables, step } = await route.handle(job);
106
152
  await this.client.completeJob(job.jobKey, variables);
107
- await this.onActivity?.({
153
+ // Job is now completed; the observer hook is isolated (emitActivity
154
+ // swallows its own errors) so it can never fall into the failure path.
155
+ await this.emitActivity({
108
156
  workflowId: route.workflowId,
109
157
  type,
110
158
  jobKey: job.jobKey,
@@ -116,7 +164,7 @@ export class Worker {
116
164
  // A handler or completion failed. Report; the engine will redeliver the
117
165
  // job after its lock times out (at-least-once → handlers must be
118
166
  // idempotent). Best-effort surface it as an incident-worthy failure.
119
- this.onError?.(e, { type });
167
+ this.emitError(e, type);
120
168
  try {
121
169
  await this.client.failJob(job.jobKey, e.message, 0);
122
170
  }
package/dist/xml.js CHANGED
@@ -22,7 +22,34 @@ export function assertIdent(kind, value) {
22
22
  export function assertWorkflowIds(wf) {
23
23
  assertIdent("workflow id", wf.id);
24
24
  if (wf.kind === "declarative") {
25
- for (const step of wf.steps)
26
- assertIdent("step name", step.name);
25
+ assertNodeNames(wf.steps);
26
+ }
27
+ }
28
+ function assertNodeNames(nodes) {
29
+ for (const node of nodes) {
30
+ switch (node.kind) {
31
+ case "run":
32
+ case "task":
33
+ case "signal":
34
+ assertIdent("step name", node.name);
35
+ break;
36
+ case "switch":
37
+ for (const c of node.cases)
38
+ assertNodeNames(c.body);
39
+ if (node.default)
40
+ assertNodeNames(node.default);
41
+ break;
42
+ case "branch":
43
+ assertNodeNames(node.then);
44
+ if (node.else)
45
+ assertNodeNames(node.else);
46
+ break;
47
+ case "loop":
48
+ assertNodeNames(node.body);
49
+ break;
50
+ case "break":
51
+ case "continue":
52
+ break;
53
+ }
27
54
  }
28
55
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nanobpm/workflow",
3
- "version": "0.1.0",
4
- "description": "Code-first durable orchestration for nanobpmn (ADR 0044). Author durable workflows as ordinary async code — either an imperative function driven by engine replay (`defineWorkflow(id, async ctx => { await ctx.run(...) })`, the Temporal-style surface) or a declarative flow with human-in-the-loop signals (`defineFlow`) — and the SDK derives the executable BPMN model, the job types, and the message/correlation wiring, then hosts a generic worker. No diagram, no task-type wiring, no correlation plumbing authored by hand. Talks to a running nanobpmn gateway over the REST v2 API.",
3
+ "version": "0.3.0",
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": {
@@ -37,8 +37,17 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.20.1",
40
+ "bpmn-auto-layout": "^1.3.0",
40
41
  "typescript": "^5.6.3"
41
42
  },
43
+ "peerDependencies": {
44
+ "bpmn-auto-layout": "^1.3.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "bpmn-auto-layout": {
48
+ "optional": true
49
+ }
50
+ },
42
51
  "publishConfig": {
43
52
  "access": "public"
44
53
  }
@@ -0,0 +1,10 @@
1
+ // Minimal ambient types for `bpmn-auto-layout` (1.3.0 ships none). `layoutProcess`
2
+ // adds diagram interchange (DI) to a BPMN XML string. The resolved shape changed
3
+ // across versions: 1.3.x resolves to the laid-out XML string; >= 1.4 / `main`
4
+ // resolves to `{ xml, warnings }`. We type the union and normalize at the call
5
+ // site (see `layout.ts`).
6
+ declare module "bpmn-auto-layout" {
7
+ export function layoutProcess(
8
+ xml: string,
9
+ ): Promise<string | { xml: string; warnings?: unknown[] }>;
10
+ }
package/src/client.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // deploy a workflow's derived model, start instances, correlate signals, and the
3
3
  // low-level job activate/complete/fail used by the Worker runtime.
4
4
 
5
- import { declarativeToBpmn } from "./declarative.js";
5
+ import { declarativeToBpmn, walkNodes } from "./declarative.js";
6
6
  import { imperativeToBpmn } from "./imperative.js";
7
7
  import type { DeclarativeFlow, DeployResult, Job, JsonObject, StartResult, Workflow } from "./types.js";
8
8
  import { assertWorkflowIds, messageName } from "./xml.js";
@@ -102,13 +102,28 @@ export class WorkflowClient {
102
102
  );
103
103
  }
104
104
 
105
- /** Correlate a signal to a parked declarative `signal` step. */
105
+ /** Correlate a signal to a parked declarative `signal` step. Fails fast on an
106
+ * unknown signal name (a typo would otherwise send an uncorrelatable message
107
+ * that the gateway silently drops). */
106
108
  async signal(
107
109
  flow: DeclarativeFlow,
108
110
  signalName: string,
109
111
  correlationKey: string,
110
112
  variables: JsonObject = {},
111
113
  ): Promise<JsonObject> {
114
+ // Signal steps can live anywhere in the tree (inside switch/branch/loop),
115
+ // so walk the whole flow, not just the top-level sequence.
116
+ const signals: string[] = [];
117
+ walkNodes(flow.steps, (n) => {
118
+ if (n.kind === "signal") signals.push(n.name);
119
+ });
120
+ if (!signals.includes(signalName)) {
121
+ throw new WorkflowError(
122
+ `unknown signal "${signalName}" on flow "${flow.id}" — declared signals: ${
123
+ signals.length ? signals.map((s) => `"${s}"`).join(", ") : "(none)"
124
+ }`,
125
+ );
126
+ }
112
127
  return this.json<JsonObject>(
113
128
  `/v2/messages/correlation`,
114
129
  {