@nanobpm/workflow 0.6.0 → 0.8.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 +13 -3
- package/dist/declarative.d.ts +73 -2
- package/dist/declarative.js +324 -18
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +57 -1
- package/dist/worker.js +3 -2
- package/dist/xml.d.ts +3 -0
- package/dist/xml.js +53 -0
- package/package.json +4 -4
- package/src/declarative.ts +421 -21
- package/src/index.ts +1 -0
- package/src/types.ts +46 -3
- package/src/worker.ts +3 -2
- package/src/xml.ts +65 -0
package/src/types.ts
CHANGED
|
@@ -21,8 +21,9 @@ export interface Job<V extends JsonObject = JsonObject> {
|
|
|
21
21
|
// --- Declarative surface (Strategy A: compile a step tree to a BPMN model) ----
|
|
22
22
|
//
|
|
23
23
|
// A flow is a TREE of nodes, not a flat list: leaf activities (`run`/`task`/
|
|
24
|
-
// `signal`) plus structural combinators (`switch`/`branch`/`loop`
|
|
25
|
-
//
|
|
24
|
+
// `signal`) plus structural combinators (`switch`/`branch`/`loop` — XOR gateways
|
|
25
|
+
// + back-edges — and `parallel`/`forEach` — parallel gateways + multi-instance)
|
|
26
|
+
// that compile to real BPMN the engine runs. See declarative.ts for the compiler.
|
|
26
27
|
|
|
27
28
|
/** The input/output data envelopes lifted onto an activity node. */
|
|
28
29
|
export interface NodeEnvelopes {
|
|
@@ -53,11 +54,50 @@ export type FlowNode =
|
|
|
53
54
|
| { kind: "run"; name: string; envelopes?: NodeEnvelopes }
|
|
54
55
|
| { kind: "task"; name: string; envelopes?: NodeEnvelopes; jobType?: string }
|
|
55
56
|
| { kind: "signal"; name: string; correlationKey: string; payload?: Envelope }
|
|
57
|
+
| ({ kind: "timer"; name: string } & TimerAt)
|
|
56
58
|
| { kind: "switch"; subject: string; cases: SwitchCase[]; default?: FlowNode[] }
|
|
57
59
|
| { kind: "branch"; condition: string; then: FlowNode[]; else?: FlowNode[] }
|
|
58
60
|
| { kind: "loop"; body: FlowNode[] }
|
|
59
61
|
| { kind: "break" }
|
|
60
|
-
| { kind: "continue" }
|
|
62
|
+
| { kind: "continue" }
|
|
63
|
+
| { kind: "parallel"; branches: FlowNode[][] }
|
|
64
|
+
| {
|
|
65
|
+
kind: "forEach";
|
|
66
|
+
/** FEEL expression evaluating to the list to fan out over. */
|
|
67
|
+
collection: string;
|
|
68
|
+
/** Variable each item is bound to in the child's scope (`inputElement`). */
|
|
69
|
+
itemVar: string;
|
|
70
|
+
body: FlowNode[];
|
|
71
|
+
/** Run children one at a time (sequential MI) instead of all at once. */
|
|
72
|
+
sequential?: boolean;
|
|
73
|
+
/** List variable that each child's `outputElement` is collected into. */
|
|
74
|
+
outputCollection?: string;
|
|
75
|
+
/** FEEL expression producing each child's contribution to `outputCollection`. */
|
|
76
|
+
outputElement?: string;
|
|
77
|
+
/** FEEL boolean that, once true, completes the body early. */
|
|
78
|
+
completionCondition?: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** A timer intermediate-catch definition: exactly one of `after` (an ISO-8601
|
|
82
|
+
* delay or FEEL expression) or `at` (an absolute instant or FEEL expression).
|
|
83
|
+
* Encoded as an XOR so downstream consumers can rely on exactly-one. */
|
|
84
|
+
export type TimerAt =
|
|
85
|
+
| { after: string; at?: never }
|
|
86
|
+
| { at: string; after?: never };
|
|
87
|
+
|
|
88
|
+
/** A flow's start-timer: the plain none start event becomes a durable timer
|
|
89
|
+
* start the engine fires on schedule. Exactly one of:
|
|
90
|
+
* - `cycle` — a recurring ISO-8601 interval (`R/PT1H`, `R5/PT30M`) or bare
|
|
91
|
+
* duration; the engine re-fires on each period. This is the model-native,
|
|
92
|
+
* durable, single-fire-per-cluster replacement for an app-side cron.
|
|
93
|
+
* - `after` — a one-shot ISO-8601 delay (`PT10S`) measured from deployment.
|
|
94
|
+
* - `at` — a one-shot absolute instant (an ISO-8601 date-time, or a FEEL
|
|
95
|
+
* `=` expression).
|
|
96
|
+
* Encoded as an XOR so the exactly-one invariant holds at the type level. */
|
|
97
|
+
export type TimerStart =
|
|
98
|
+
| { cycle: string; after?: never; at?: never }
|
|
99
|
+
| { after: string; cycle?: never; at?: never }
|
|
100
|
+
| { at: string; cycle?: never; after?: never };
|
|
61
101
|
|
|
62
102
|
/** One case of a `switch`: routed when `subject = value` (FEEL equality). */
|
|
63
103
|
export interface SwitchCase {
|
|
@@ -75,6 +115,9 @@ export interface DeclarativeFlow {
|
|
|
75
115
|
/** The flow's node tree (top-level sequence). */
|
|
76
116
|
steps: FlowNode[];
|
|
77
117
|
handlers: Record<string, StepHandler>;
|
|
118
|
+
/** When set, the flow's start event is a durable timer start (see
|
|
119
|
+
* {@link TimerStart}) rather than a plain none start. */
|
|
120
|
+
startTimer?: TimerStart;
|
|
78
121
|
}
|
|
79
122
|
|
|
80
123
|
// --- Imperative surface (Strategy B: a replayed orchestration function) -------
|
package/src/worker.ts
CHANGED
|
@@ -112,8 +112,9 @@ export class Worker {
|
|
|
112
112
|
} else {
|
|
113
113
|
// Walk the flow tree; only `run` steps are hosted locally. `signal`
|
|
114
114
|
// (message catch) and `task` (external worker) contribute no in-process
|
|
115
|
-
// route, and structural combinators (`switch`/`branch`/`loop
|
|
116
|
-
// routing with no job of their own
|
|
115
|
+
// route, and structural combinators (`switch`/`branch`/`loop`/`parallel`/
|
|
116
|
+
// `forEach`) are pure routing with no job of their own — but the `run`
|
|
117
|
+
// steps NESTED inside them are hosted, since `walkNodes` recurses.
|
|
117
118
|
walkNodes(wf.steps, (s) => {
|
|
118
119
|
if (s.kind !== "run") return;
|
|
119
120
|
const handler = wf.handlers[s.name];
|
package/src/xml.ts
CHANGED
|
@@ -40,6 +40,68 @@ export function assertJobType(kind: string, value: string): void {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// --- Timer definitions (BPMN timerEventDefinition bodies) --------------------
|
|
44
|
+
//
|
|
45
|
+
// The engine (engine-core/src/bpmn.rs) accepts a nested `timeDuration`,
|
|
46
|
+
// `timeCycle`, or `timeDate` as either a static ISO-8601 literal or a FEEL
|
|
47
|
+
// expression (a leading `=`). We validate the literal forms at authoring time so
|
|
48
|
+
// a malformed timer fails fast in the DSL rather than at deploy.
|
|
49
|
+
|
|
50
|
+
/** An ISO-8601 duration with integer week/day/hour/minute/second components
|
|
51
|
+
* (no years/months — ambiguous length — matching the engine's parser). */
|
|
52
|
+
const ISO_DURATION = /^P(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?$/;
|
|
53
|
+
function isIsoDuration(v: string): boolean {
|
|
54
|
+
if (!ISO_DURATION.test(v) || v === "P" || v === "PT" || v.endsWith("T")) return false;
|
|
55
|
+
return /\d/.test(v);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A recurring ISO-8601 interval: an optional `R[n]/` repeat prefix + a
|
|
59
|
+
* duration, or a bare duration (the engine repeats it unboundedly). */
|
|
60
|
+
function isIsoCycle(v: string): boolean {
|
|
61
|
+
const slash = v.indexOf("/");
|
|
62
|
+
if (slash < 0) return isIsoDuration(v);
|
|
63
|
+
const repeat = v.slice(0, slash);
|
|
64
|
+
const interval = v.slice(slash + 1);
|
|
65
|
+
return /^R\d*$/.test(repeat) && isIsoDuration(interval);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** An ISO-8601 date-time instant (a `timeDate`). A `timeDate` is an absolute
|
|
69
|
+
* instant, so a zone designator (`Z` or an explicit `±HH:MM` offset) is
|
|
70
|
+
* required — a bare local datetime like `2026-01-01T09:00:00` is ambiguous. */
|
|
71
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
72
|
+
|
|
73
|
+
/** A FEEL expression body (a leading `=`, non-empty). */
|
|
74
|
+
function isFeel(v: string): boolean {
|
|
75
|
+
return v.startsWith("=") && v.length > 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function assertTimerDuration(kind: string, value: string): void {
|
|
79
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
80
|
+
if (v === "" || (!isFeel(v) && !isIsoDuration(v))) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${kind} "${value}" is not an ISO-8601 duration (e.g. PT1M30S, P1DT6H) or a FEEL expression (=...)`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function assertTimerCycle(kind: string, value: string): void {
|
|
88
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
89
|
+
if (v === "" || (!isFeel(v) && !isIsoCycle(v))) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`${kind} "${value}" is not an ISO-8601 repeating interval (e.g. R/PT1H, R5/PT30M), a bare duration (e.g. PT1H), or a FEEL expression (=...)`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function assertTimerDate(kind: string, value: string): void {
|
|
97
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
98
|
+
if (v === "" || (!isFeel(v) && !ISO_DATE.test(v))) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`${kind} "${value}" is not an ISO-8601 instant (e.g. 2026-01-01T09:00:00Z) or a FEEL expression (=...)`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
43
105
|
export function assertWorkflowIds(wf: Workflow): void {
|
|
44
106
|
assertIdent("workflow id", wf.id);
|
|
45
107
|
if (wf.kind === "declarative") {
|
|
@@ -54,6 +116,9 @@ function assertNodeNames(nodes: FlowNode[]): void {
|
|
|
54
116
|
case "signal":
|
|
55
117
|
assertIdent("step name", node.name);
|
|
56
118
|
break;
|
|
119
|
+
case "timer":
|
|
120
|
+
assertIdent("step name", node.name);
|
|
121
|
+
break;
|
|
57
122
|
case "task":
|
|
58
123
|
assertIdent("step name", node.name);
|
|
59
124
|
if (node.jobType !== undefined) assertJobType("job type", node.jobType);
|