@nanobpm/workflow 0.10.0 → 0.11.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 +7 -0
- package/dist/declarative.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/nodes/boundary.d.ts +46 -0
- package/dist/nodes/boundary.js +92 -0
- package/dist/nodes/index.d.ts +1 -0
- package/dist/nodes/index.js +1 -0
- package/package.json +1 -1
- package/src/declarative.ts +1 -0
- package/src/index.ts +3 -0
- package/src/nodes/boundary.ts +149 -0
- package/src/nodes/index.ts +1 -0
package/README.md
CHANGED
|
@@ -62,6 +62,13 @@ human-in-the-loop path), composed with control-flow combinators:
|
|
|
62
62
|
(a message- or timer-event-definition), the arm bodies rejoining downstream by an
|
|
63
63
|
implicit XOR merge. This is the shape a convergence loop uses to bound a
|
|
64
64
|
human-in-the-loop wait with a timeout.
|
|
65
|
+
- `w.run(...).boundary({ timer, onTimeout, interrupting?, name? })` — attach an
|
|
66
|
+
SLA (an interrupting timer boundary event) to the PRECEDING activity: when
|
|
67
|
+
`timer` elapses the activity is cancelled and the token routes to the
|
|
68
|
+
`onTimeout` escalation body, which then converges with the activity's normal
|
|
69
|
+
continuation. `timer` is an ISO-8601 duration (`PT24H`) or a FEEL
|
|
70
|
+
`=`-expression (`=escalationSlaTimeout`). `interrupting` defaults to `true`
|
|
71
|
+
(pass `false` for a non-interrupting boundary that leaves the activity running).
|
|
65
72
|
|
|
66
73
|
```ts
|
|
67
74
|
import { defineFlow, WorkflowClient, Worker } from "@nanobpm/workflow";
|
package/dist/declarative.js
CHANGED
|
@@ -577,6 +577,7 @@ class Compiler {
|
|
|
577
577
|
const shapeXml = this.emitShapes();
|
|
578
578
|
return (`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
579
579
|
`<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" ` +
|
|
580
|
+
`xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ` +
|
|
580
581
|
`xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" ` +
|
|
581
582
|
`xmlns:nano="https://nanobpm.io/schema/shapes/1.0" ` +
|
|
582
583
|
`id="Definitions_${escapeXml(this.flow.id)}" targetNamespace="http://bpmn.io/schema/bpmn">\n` +
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { defineWorkflow, imperativeToBpmn, replayOnce } from "./imperative.js";
|
|
|
2
2
|
export type { Journal, ReplayStep } from "./imperative.js";
|
|
3
3
|
export { defineFlow, declarativeToBpmn, externalJobTypes, walkNodes } from "./declarative.js";
|
|
4
4
|
export type { FlowBuilder } from "./declarative.js";
|
|
5
|
+
export type { BoundaryOptions } from "./nodes/boundary.js";
|
|
5
6
|
export { layoutBpmn, declarativeToLayoutedBpmn } from "./layout.js";
|
|
6
7
|
export { envelope } from "./envelope.js";
|
|
7
8
|
export type { Envelope, EnvelopeType, EnvelopeField, FieldSpec, ScalarType, ScalarTs, FieldTs, } from "./envelope.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Block } from "../declarative.js";
|
|
2
|
+
/** Options for `.boundary(...)`: an attached timer SLA on the preceding activity.
|
|
3
|
+
* - `timer` — the timeout: an ISO-8601 duration (`PT24H`, `P1DT6H`) or a FEEL
|
|
4
|
+
* `=`-expression (`=escalationSlaTimeout`); the duration need not be a literal.
|
|
5
|
+
* - `onTimeout` — the escalation body run when the timer elapses (its danglers
|
|
6
|
+
* converge with the activity's normal continuation).
|
|
7
|
+
* - `interrupting` — whether firing cancels the host activity. Defaults to
|
|
8
|
+
* `true` (an interrupting boundary; `cancelActivity` omitted, the BPMN default).
|
|
9
|
+
* Pass `false` for a non-interrupting boundary (`cancelActivity="false"`).
|
|
10
|
+
* - `name` — an optional label for the boundary event. */
|
|
11
|
+
export interface BoundaryOptions<C extends object = object> {
|
|
12
|
+
timer: string;
|
|
13
|
+
onTimeout: Block<C>;
|
|
14
|
+
interrupting?: boolean;
|
|
15
|
+
name?: string;
|
|
16
|
+
}
|
|
17
|
+
declare module "../types.js" {
|
|
18
|
+
interface FlowNodeRegistry {
|
|
19
|
+
boundary: {
|
|
20
|
+
kind: "boundary";
|
|
21
|
+
/** The derived element id of the activity this boundary attaches to. */
|
|
22
|
+
attachedTo: string;
|
|
23
|
+
/** The timeout — an ISO-8601 duration or a FEEL `=`-expression. */
|
|
24
|
+
timer: string;
|
|
25
|
+
/** Whether firing cancels the host (interrupting). */
|
|
26
|
+
interrupting: boolean;
|
|
27
|
+
/** The escalation body wired from the boundary event's outgoing flow. */
|
|
28
|
+
onTimeout: FlowNode[];
|
|
29
|
+
/** Optional boundary-event label. */
|
|
30
|
+
label?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
declare module "../declarative.js" {
|
|
35
|
+
interface FlowBuilder<C extends object> {
|
|
36
|
+
/**
|
|
37
|
+
* Attach an interrupting timer boundary event (an SLA) to the PRECEDING
|
|
38
|
+
* activity (a `run`/`task`). When `timer` elapses, the activity is
|
|
39
|
+
* cancelled (for an interrupting boundary) and the token routes to the
|
|
40
|
+
* `onTimeout` escalation body; that body then converges with the activity's
|
|
41
|
+
* normal continuation. `timer` is an ISO-8601 duration or a FEEL
|
|
42
|
+
* `=`-expression. Chains as `w.run(...).boundary({ timer, onTimeout })`.
|
|
43
|
+
*/
|
|
44
|
+
boundary(opts: BoundaryOptions<C>): FlowBuilder<C>;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Flow-node kind: `boundary` — an activity-level attached interrupting timer
|
|
2
|
+
// boundary event (an SLA). Chained onto the activity it guards, it attaches a
|
|
3
|
+
// `<bpmn:boundaryEvent … attachedToRef=… ><bpmn:timerEventDefinition>` to that
|
|
4
|
+
// activity: when the timer elapses the activity is cancelled (interrupting) and
|
|
5
|
+
// the token routes to the `onTimeout` escalation body, which then converges with
|
|
6
|
+
// the activity's normal continuation. Contributed entirely from this module
|
|
7
|
+
// through the extension seam (epic #314, S2/#317) — NO edit to the shared
|
|
8
|
+
// `FlowNode` union, the `walkNodes`/`emitNode` dispatch, or the generated barrel.
|
|
9
|
+
import { outgoingOnly } from "../declarative.js";
|
|
10
|
+
import { assertTimerDuration, escapeXml } from "../xml.js";
|
|
11
|
+
import { registerNodeKind } from "./registry.js";
|
|
12
|
+
/** The flow-node kinds a boundary event may attach to. BPMN restricts boundary
|
|
13
|
+
* events to ACTIVITIES, so this is the set of activity kinds (the ones that
|
|
14
|
+
* render as a `<bpmn:serviceTask>`) — NOT merely "any node with a `name`", which
|
|
15
|
+
* would also admit catch events like `signal`/`timer` and produce invalid BPMN. */
|
|
16
|
+
const BOUNDARY_HOST_KINDS = new Set(["run", "task"]);
|
|
17
|
+
/** True for a FEEL expression body (a leading `=`), which renders with an
|
|
18
|
+
* `xsi:type="bpmn:tFormalExpression"` marker on the `timeDuration`. */
|
|
19
|
+
function isFeel(value) {
|
|
20
|
+
return value.startsWith("=");
|
|
21
|
+
}
|
|
22
|
+
function renderBoundary(id, node, outg) {
|
|
23
|
+
const nameAttr = node.label ? ` name="${escapeXml(node.label)}"` : "";
|
|
24
|
+
// An interrupting boundary omits `cancelActivity` (its BPMN default is true),
|
|
25
|
+
// matching the hand-authored nwf goldens; a non-interrupting one is explicit.
|
|
26
|
+
const cancelAttr = node.interrupting ? "" : ` cancelActivity="false"`;
|
|
27
|
+
const durType = isFeel(node.timer) ? ` xsi:type="bpmn:tFormalExpression"` : "";
|
|
28
|
+
return (` <bpmn:boundaryEvent id="${id}"${nameAttr} attachedToRef="${escapeXml(node.attachedTo)}"${cancelAttr}>\n` +
|
|
29
|
+
` ${outgoingOnly(outg)}\n` +
|
|
30
|
+
` <bpmn:timerEventDefinition>\n` +
|
|
31
|
+
` <bpmn:timeDuration${durType}>${escapeXml(node.timer)}</bpmn:timeDuration>\n` +
|
|
32
|
+
` </bpmn:timerEventDefinition>\n` +
|
|
33
|
+
` </bpmn:boundaryEvent>`);
|
|
34
|
+
}
|
|
35
|
+
registerNodeKind("boundary", {
|
|
36
|
+
build: (api) => (opts) => {
|
|
37
|
+
if (!opts || typeof opts !== "object") {
|
|
38
|
+
throw new Error(`boundary(...) needs { timer, onTimeout }`);
|
|
39
|
+
}
|
|
40
|
+
if (typeof opts.timer !== "string" || opts.timer.trim() === "") {
|
|
41
|
+
throw new Error(`boundary(...) needs a { timer } duration (an ISO-8601 duration or a FEEL =expression)`);
|
|
42
|
+
}
|
|
43
|
+
const duration = opts.timer.trim();
|
|
44
|
+
assertTimerDuration("boundary(...) timer", duration);
|
|
45
|
+
if (typeof opts.onTimeout !== "function") {
|
|
46
|
+
throw new Error(`boundary(...) needs an { onTimeout } escalation body (b) => {…}`);
|
|
47
|
+
}
|
|
48
|
+
if (opts.interrupting !== undefined && typeof opts.interrupting !== "boolean") {
|
|
49
|
+
throw new Error(`boundary(...) { interrupting } must be a boolean`);
|
|
50
|
+
}
|
|
51
|
+
if (opts.name !== undefined && typeof opts.name !== "string") {
|
|
52
|
+
throw new Error(`boundary(...) { name } must be a string`);
|
|
53
|
+
}
|
|
54
|
+
const host = api.out[api.out.length - 1];
|
|
55
|
+
if (!host) {
|
|
56
|
+
throw new Error(`boundary(...) must follow the activity it attaches to (e.g. w.run(...).boundary(...))`);
|
|
57
|
+
}
|
|
58
|
+
if (!BOUNDARY_HOST_KINDS.has(host.kind) || !("name" in host) || typeof host.name !== "string") {
|
|
59
|
+
throw new Error(`boundary(...) can only attach to a named activity (run/task), not a "${host.kind}"`);
|
|
60
|
+
}
|
|
61
|
+
const onTimeout = api.child(opts.onTimeout, false);
|
|
62
|
+
if (onTimeout.length === 0) {
|
|
63
|
+
throw new Error(`boundary(...) needs a non-empty { onTimeout } escalation body (its outgoing flow has nowhere to go)`);
|
|
64
|
+
}
|
|
65
|
+
api.out.push({
|
|
66
|
+
kind: "boundary",
|
|
67
|
+
attachedTo: host.name,
|
|
68
|
+
timer: duration,
|
|
69
|
+
interrupting: opts.interrupting ?? true,
|
|
70
|
+
onTimeout,
|
|
71
|
+
label: opts.name,
|
|
72
|
+
});
|
|
73
|
+
return api.self();
|
|
74
|
+
},
|
|
75
|
+
walk: (node, recurse) => {
|
|
76
|
+
recurse(node.onTimeout);
|
|
77
|
+
},
|
|
78
|
+
emit: (node, incoming, loop, api) => {
|
|
79
|
+
// A boundary event has NO incoming sequence flow — it attaches to its host
|
|
80
|
+
// via `attachedToRef`. Place it, wire its outgoing to the escalation body,
|
|
81
|
+
// and return the host's normal continuation (`incoming`, passed through)
|
|
82
|
+
// PLUS the escalation danglers, so both converge on whatever follows.
|
|
83
|
+
const beId = `Be_${api.nextGw()}`;
|
|
84
|
+
api.addNode({
|
|
85
|
+
id: beId,
|
|
86
|
+
render: (_inc, outg) => renderBoundary(beId, node, outg),
|
|
87
|
+
});
|
|
88
|
+
const escalation = api.newEdge(beId);
|
|
89
|
+
const escOut = api.emitList(node.onTimeout, [escalation], loop);
|
|
90
|
+
return [...incoming, ...escOut];
|
|
91
|
+
},
|
|
92
|
+
});
|
package/dist/nodes/index.d.ts
CHANGED
package/dist/nodes/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// table `makeBuilder`/`walkNodes`/the emitter route through. A new kind is
|
|
6
6
|
// picked up automatically by re-running the generator (a prebuild step) — no
|
|
7
7
|
// hand-edit here.
|
|
8
|
+
import "./boundary.js";
|
|
8
9
|
import "./branch.js";
|
|
9
10
|
import "./break.js";
|
|
10
11
|
import "./continue.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|
package/src/declarative.ts
CHANGED
|
@@ -933,6 +933,7 @@ class Compiler {
|
|
|
933
933
|
return (
|
|
934
934
|
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
935
935
|
`<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" ` +
|
|
936
|
+
`xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ` +
|
|
936
937
|
`xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" ` +
|
|
937
938
|
`xmlns:nano="https://nanobpm.io/schema/shapes/1.0" ` +
|
|
938
939
|
`id="Definitions_${escapeXml(this.flow.id)}" targetNamespace="http://bpmn.io/schema/bpmn">\n` +
|
package/src/index.ts
CHANGED
|
@@ -43,6 +43,9 @@ export { defineWorkflow, imperativeToBpmn, replayOnce } from "./imperative.js";
|
|
|
43
43
|
export type { Journal, ReplayStep } from "./imperative.js";
|
|
44
44
|
export { defineFlow, declarativeToBpmn, externalJobTypes, walkNodes } from "./declarative.js";
|
|
45
45
|
export type { FlowBuilder } from "./declarative.js";
|
|
46
|
+
// The `.boundary(...)` option type (its builder method augments FlowBuilder from
|
|
47
|
+
// the boundary kind module — S2/#317).
|
|
48
|
+
export type { BoundaryOptions } from "./nodes/boundary.js";
|
|
46
49
|
export { layoutBpmn, declarativeToLayoutedBpmn } from "./layout.js";
|
|
47
50
|
export { envelope } from "./envelope.js";
|
|
48
51
|
export type {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Flow-node kind: `boundary` — an activity-level attached interrupting timer
|
|
2
|
+
// boundary event (an SLA). Chained onto the activity it guards, it attaches a
|
|
3
|
+
// `<bpmn:boundaryEvent … attachedToRef=… ><bpmn:timerEventDefinition>` to that
|
|
4
|
+
// activity: when the timer elapses the activity is cancelled (interrupting) and
|
|
5
|
+
// the token routes to the `onTimeout` escalation body, which then converges with
|
|
6
|
+
// the activity's normal continuation. Contributed entirely from this module
|
|
7
|
+
// through the extension seam (epic #314, S2/#317) — NO edit to the shared
|
|
8
|
+
// `FlowNode` union, the `walkNodes`/`emitNode` dispatch, or the generated barrel.
|
|
9
|
+
|
|
10
|
+
import type { FlowNode } from "../types.js";
|
|
11
|
+
import type { Block, Edge } from "../declarative.js";
|
|
12
|
+
import { outgoingOnly } from "../declarative.js";
|
|
13
|
+
import { assertTimerDuration, escapeXml } from "../xml.js";
|
|
14
|
+
import { registerNodeKind } from "./registry.js";
|
|
15
|
+
|
|
16
|
+
/** Options for `.boundary(...)`: an attached timer SLA on the preceding activity.
|
|
17
|
+
* - `timer` — the timeout: an ISO-8601 duration (`PT24H`, `P1DT6H`) or a FEEL
|
|
18
|
+
* `=`-expression (`=escalationSlaTimeout`); the duration need not be a literal.
|
|
19
|
+
* - `onTimeout` — the escalation body run when the timer elapses (its danglers
|
|
20
|
+
* converge with the activity's normal continuation).
|
|
21
|
+
* - `interrupting` — whether firing cancels the host activity. Defaults to
|
|
22
|
+
* `true` (an interrupting boundary; `cancelActivity` omitted, the BPMN default).
|
|
23
|
+
* Pass `false` for a non-interrupting boundary (`cancelActivity="false"`).
|
|
24
|
+
* - `name` — an optional label for the boundary event. */
|
|
25
|
+
export interface BoundaryOptions<C extends object = object> {
|
|
26
|
+
timer: string;
|
|
27
|
+
onTimeout: Block<C>;
|
|
28
|
+
interrupting?: boolean;
|
|
29
|
+
name?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare module "../types.js" {
|
|
33
|
+
interface FlowNodeRegistry {
|
|
34
|
+
boundary: {
|
|
35
|
+
kind: "boundary";
|
|
36
|
+
/** The derived element id of the activity this boundary attaches to. */
|
|
37
|
+
attachedTo: string;
|
|
38
|
+
/** The timeout — an ISO-8601 duration or a FEEL `=`-expression. */
|
|
39
|
+
timer: string;
|
|
40
|
+
/** Whether firing cancels the host (interrupting). */
|
|
41
|
+
interrupting: boolean;
|
|
42
|
+
/** The escalation body wired from the boundary event's outgoing flow. */
|
|
43
|
+
onTimeout: FlowNode[];
|
|
44
|
+
/** Optional boundary-event label. */
|
|
45
|
+
label?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare module "../declarative.js" {
|
|
51
|
+
interface FlowBuilder<C extends object> {
|
|
52
|
+
/**
|
|
53
|
+
* Attach an interrupting timer boundary event (an SLA) to the PRECEDING
|
|
54
|
+
* activity (a `run`/`task`). When `timer` elapses, the activity is
|
|
55
|
+
* cancelled (for an interrupting boundary) and the token routes to the
|
|
56
|
+
* `onTimeout` escalation body; that body then converges with the activity's
|
|
57
|
+
* normal continuation. `timer` is an ISO-8601 duration or a FEEL
|
|
58
|
+
* `=`-expression. Chains as `w.run(...).boundary({ timer, onTimeout })`.
|
|
59
|
+
*/
|
|
60
|
+
boundary(opts: BoundaryOptions<C>): FlowBuilder<C>;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The flow-node kinds a boundary event may attach to. BPMN restricts boundary
|
|
65
|
+
* events to ACTIVITIES, so this is the set of activity kinds (the ones that
|
|
66
|
+
* render as a `<bpmn:serviceTask>`) — NOT merely "any node with a `name`", which
|
|
67
|
+
* would also admit catch events like `signal`/`timer` and produce invalid BPMN. */
|
|
68
|
+
const BOUNDARY_HOST_KINDS = new Set(["run", "task"]);
|
|
69
|
+
|
|
70
|
+
/** True for a FEEL expression body (a leading `=`), which renders with an
|
|
71
|
+
* `xsi:type="bpmn:tFormalExpression"` marker on the `timeDuration`. */
|
|
72
|
+
function isFeel(value: string): boolean {
|
|
73
|
+
return value.startsWith("=");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderBoundary(id: string, node: Extract<FlowNode, { kind: "boundary" }>, outg: string[]): string {
|
|
77
|
+
const nameAttr = node.label ? ` name="${escapeXml(node.label)}"` : "";
|
|
78
|
+
// An interrupting boundary omits `cancelActivity` (its BPMN default is true),
|
|
79
|
+
// matching the hand-authored nwf goldens; a non-interrupting one is explicit.
|
|
80
|
+
const cancelAttr = node.interrupting ? "" : ` cancelActivity="false"`;
|
|
81
|
+
const durType = isFeel(node.timer) ? ` xsi:type="bpmn:tFormalExpression"` : "";
|
|
82
|
+
return (
|
|
83
|
+
` <bpmn:boundaryEvent id="${id}"${nameAttr} attachedToRef="${escapeXml(node.attachedTo)}"${cancelAttr}>\n` +
|
|
84
|
+
` ${outgoingOnly(outg)}\n` +
|
|
85
|
+
` <bpmn:timerEventDefinition>\n` +
|
|
86
|
+
` <bpmn:timeDuration${durType}>${escapeXml(node.timer)}</bpmn:timeDuration>\n` +
|
|
87
|
+
` </bpmn:timerEventDefinition>\n` +
|
|
88
|
+
` </bpmn:boundaryEvent>`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
registerNodeKind("boundary", {
|
|
93
|
+
build: (api) => (opts: BoundaryOptions) => {
|
|
94
|
+
if (!opts || typeof opts !== "object") {
|
|
95
|
+
throw new Error(`boundary(...) needs { timer, onTimeout }`);
|
|
96
|
+
}
|
|
97
|
+
if (typeof opts.timer !== "string" || opts.timer.trim() === "") {
|
|
98
|
+
throw new Error(`boundary(...) needs a { timer } duration (an ISO-8601 duration or a FEEL =expression)`);
|
|
99
|
+
}
|
|
100
|
+
const duration = opts.timer.trim();
|
|
101
|
+
assertTimerDuration("boundary(...) timer", duration);
|
|
102
|
+
if (typeof opts.onTimeout !== "function") {
|
|
103
|
+
throw new Error(`boundary(...) needs an { onTimeout } escalation body (b) => {…}`);
|
|
104
|
+
}
|
|
105
|
+
if (opts.interrupting !== undefined && typeof opts.interrupting !== "boolean") {
|
|
106
|
+
throw new Error(`boundary(...) { interrupting } must be a boolean`);
|
|
107
|
+
}
|
|
108
|
+
if (opts.name !== undefined && typeof opts.name !== "string") {
|
|
109
|
+
throw new Error(`boundary(...) { name } must be a string`);
|
|
110
|
+
}
|
|
111
|
+
const host = api.out[api.out.length - 1];
|
|
112
|
+
if (!host) {
|
|
113
|
+
throw new Error(`boundary(...) must follow the activity it attaches to (e.g. w.run(...).boundary(...))`);
|
|
114
|
+
}
|
|
115
|
+
if (!BOUNDARY_HOST_KINDS.has(host.kind) || !("name" in host) || typeof host.name !== "string") {
|
|
116
|
+
throw new Error(`boundary(...) can only attach to a named activity (run/task), not a "${host.kind}"`);
|
|
117
|
+
}
|
|
118
|
+
const onTimeout = api.child(opts.onTimeout, false);
|
|
119
|
+
if (onTimeout.length === 0) {
|
|
120
|
+
throw new Error(`boundary(...) needs a non-empty { onTimeout } escalation body (its outgoing flow has nowhere to go)`);
|
|
121
|
+
}
|
|
122
|
+
api.out.push({
|
|
123
|
+
kind: "boundary",
|
|
124
|
+
attachedTo: host.name,
|
|
125
|
+
timer: duration,
|
|
126
|
+
interrupting: opts.interrupting ?? true,
|
|
127
|
+
onTimeout,
|
|
128
|
+
label: opts.name,
|
|
129
|
+
});
|
|
130
|
+
return api.self();
|
|
131
|
+
},
|
|
132
|
+
walk: (node, recurse) => {
|
|
133
|
+
recurse(node.onTimeout);
|
|
134
|
+
},
|
|
135
|
+
emit: (node, incoming, loop, api) => {
|
|
136
|
+
// A boundary event has NO incoming sequence flow — it attaches to its host
|
|
137
|
+
// via `attachedToRef`. Place it, wire its outgoing to the escalation body,
|
|
138
|
+
// and return the host's normal continuation (`incoming`, passed through)
|
|
139
|
+
// PLUS the escalation danglers, so both converge on whatever follows.
|
|
140
|
+
const beId = `Be_${api.nextGw()}`;
|
|
141
|
+
api.addNode({
|
|
142
|
+
id: beId,
|
|
143
|
+
render: (_inc, outg) => renderBoundary(beId, node, outg),
|
|
144
|
+
});
|
|
145
|
+
const escalation: Edge = api.newEdge(beId);
|
|
146
|
+
const escOut = api.emitList(node.onTimeout, [escalation], loop);
|
|
147
|
+
return [...incoming, ...escOut];
|
|
148
|
+
},
|
|
149
|
+
});
|