@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/dist/types.d.ts CHANGED
@@ -52,7 +52,10 @@ export type FlowNode = {
52
52
  name: string;
53
53
  correlationKey: string;
54
54
  payload?: Envelope;
55
- } | {
55
+ } | ({
56
+ kind: "timer";
57
+ name: string;
58
+ } & TimerAt) | {
56
59
  kind: "switch";
57
60
  subject: string;
58
61
  cases: SwitchCase[];
@@ -69,6 +72,56 @@ export type FlowNode = {
69
72
  kind: "break";
70
73
  } | {
71
74
  kind: "continue";
75
+ } | {
76
+ kind: "parallel";
77
+ branches: FlowNode[][];
78
+ } | {
79
+ kind: "forEach";
80
+ /** FEEL expression evaluating to the list to fan out over. */
81
+ collection: string;
82
+ /** Variable each item is bound to in the child's scope (`inputElement`). */
83
+ itemVar: string;
84
+ body: FlowNode[];
85
+ /** Run children one at a time (sequential MI) instead of all at once. */
86
+ sequential?: boolean;
87
+ /** List variable that each child's `outputElement` is collected into. */
88
+ outputCollection?: string;
89
+ /** FEEL expression producing each child's contribution to `outputCollection`. */
90
+ outputElement?: string;
91
+ /** FEEL boolean that, once true, completes the body early. */
92
+ completionCondition?: string;
93
+ };
94
+ /** A timer intermediate-catch definition: exactly one of `after` (an ISO-8601
95
+ * delay or FEEL expression) or `at` (an absolute instant or FEEL expression).
96
+ * Encoded as an XOR so downstream consumers can rely on exactly-one. */
97
+ export type TimerAt = {
98
+ after: string;
99
+ at?: never;
100
+ } | {
101
+ at: string;
102
+ after?: never;
103
+ };
104
+ /** A flow's start-timer: the plain none start event becomes a durable timer
105
+ * start the engine fires on schedule. Exactly one of:
106
+ * - `cycle` — a recurring ISO-8601 interval (`R/PT1H`, `R5/PT30M`) or bare
107
+ * duration; the engine re-fires on each period. This is the model-native,
108
+ * durable, single-fire-per-cluster replacement for an app-side cron.
109
+ * - `after` — a one-shot ISO-8601 delay (`PT10S`) measured from deployment.
110
+ * - `at` — a one-shot absolute instant (an ISO-8601 date-time, or a FEEL
111
+ * `=` expression).
112
+ * Encoded as an XOR so the exactly-one invariant holds at the type level. */
113
+ export type TimerStart = {
114
+ cycle: string;
115
+ after?: never;
116
+ at?: never;
117
+ } | {
118
+ after: string;
119
+ cycle?: never;
120
+ at?: never;
121
+ } | {
122
+ at: string;
123
+ cycle?: never;
124
+ after?: never;
72
125
  };
73
126
  /** One case of a `switch`: routed when `subject = value` (FEEL equality). */
74
127
  export interface SwitchCase {
@@ -84,6 +137,9 @@ export interface DeclarativeFlow {
84
137
  /** The flow's node tree (top-level sequence). */
85
138
  steps: FlowNode[];
86
139
  handlers: Record<string, StepHandler>;
140
+ /** When set, the flow's start event is a durable timer start (see
141
+ * {@link TimerStart}) rather than a plain none start. */
142
+ startTimer?: TimerStart;
87
143
  }
88
144
  /** The context passed to an imperative orchestration function. */
89
145
  export interface WorkflowContext {
package/dist/worker.js CHANGED
@@ -60,8 +60,9 @@ export class Worker {
60
60
  else {
61
61
  // Walk the flow tree; only `run` steps are hosted locally. `signal`
62
62
  // (message catch) and `task` (external worker) contribute no in-process
63
- // route, and structural combinators (`switch`/`branch`/`loop`) are pure
64
- // routing with no job of their own.
63
+ // route, and structural combinators (`switch`/`branch`/`loop`/`parallel`/
64
+ // `forEach`) are pure routing with no job of their own — but the `run`
65
+ // steps NESTED inside them are hosted, since `walkNodes` recurses.
65
66
  walkNodes(wf.steps, (s) => {
66
67
  if (s.kind !== "run")
67
68
  return;
package/dist/xml.d.ts CHANGED
@@ -8,4 +8,7 @@ export declare const messageName: (workflowId: string, step: string) => string;
8
8
  export declare const orchestrateType: (workflowId: string) => string;
9
9
  export declare function assertIdent(kind: string, value: string): void;
10
10
  export declare function assertJobType(kind: string, value: string): void;
11
+ export declare function assertTimerDuration(kind: string, value: string): void;
12
+ export declare function assertTimerCycle(kind: string, value: string): void;
13
+ export declare function assertTimerDate(kind: string, value: string): void;
11
14
  export declare function assertWorkflowIds(wf: Workflow): void;
package/dist/xml.js CHANGED
@@ -29,6 +29,56 @@ export function assertJobType(kind, value) {
29
29
  throw new Error(`${kind} "${value}" is not a valid job type (expected a non-empty token of letters, digits, and _ . : + -)`);
30
30
  }
31
31
  }
32
+ // --- Timer definitions (BPMN timerEventDefinition bodies) --------------------
33
+ //
34
+ // The engine (engine-core/src/bpmn.rs) accepts a nested `timeDuration`,
35
+ // `timeCycle`, or `timeDate` as either a static ISO-8601 literal or a FEEL
36
+ // expression (a leading `=`). We validate the literal forms at authoring time so
37
+ // a malformed timer fails fast in the DSL rather than at deploy.
38
+ /** An ISO-8601 duration with integer week/day/hour/minute/second components
39
+ * (no years/months — ambiguous length — matching the engine's parser). */
40
+ const ISO_DURATION = /^P(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?$/;
41
+ function isIsoDuration(v) {
42
+ if (!ISO_DURATION.test(v) || v === "P" || v === "PT" || v.endsWith("T"))
43
+ return false;
44
+ return /\d/.test(v);
45
+ }
46
+ /** A recurring ISO-8601 interval: an optional `R[n]/` repeat prefix + a
47
+ * duration, or a bare duration (the engine repeats it unboundedly). */
48
+ function isIsoCycle(v) {
49
+ const slash = v.indexOf("/");
50
+ if (slash < 0)
51
+ return isIsoDuration(v);
52
+ const repeat = v.slice(0, slash);
53
+ const interval = v.slice(slash + 1);
54
+ return /^R\d*$/.test(repeat) && isIsoDuration(interval);
55
+ }
56
+ /** An ISO-8601 date-time instant (a `timeDate`). A `timeDate` is an absolute
57
+ * instant, so a zone designator (`Z` or an explicit `±HH:MM` offset) is
58
+ * required — a bare local datetime like `2026-01-01T09:00:00` is ambiguous. */
59
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
60
+ /** A FEEL expression body (a leading `=`, non-empty). */
61
+ function isFeel(v) {
62
+ return v.startsWith("=") && v.length > 1;
63
+ }
64
+ export function assertTimerDuration(kind, value) {
65
+ const v = typeof value === "string" ? value.trim() : "";
66
+ if (v === "" || (!isFeel(v) && !isIsoDuration(v))) {
67
+ throw new Error(`${kind} "${value}" is not an ISO-8601 duration (e.g. PT1M30S, P1DT6H) or a FEEL expression (=...)`);
68
+ }
69
+ }
70
+ export function assertTimerCycle(kind, value) {
71
+ const v = typeof value === "string" ? value.trim() : "";
72
+ if (v === "" || (!isFeel(v) && !isIsoCycle(v))) {
73
+ throw new Error(`${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 (=...)`);
74
+ }
75
+ }
76
+ export function assertTimerDate(kind, value) {
77
+ const v = typeof value === "string" ? value.trim() : "";
78
+ if (v === "" || (!isFeel(v) && !ISO_DATE.test(v))) {
79
+ throw new Error(`${kind} "${value}" is not an ISO-8601 instant (e.g. 2026-01-01T09:00:00Z) or a FEEL expression (=...)`);
80
+ }
81
+ }
32
82
  export function assertWorkflowIds(wf) {
33
83
  assertIdent("workflow id", wf.id);
34
84
  if (wf.kind === "declarative") {
@@ -42,6 +92,9 @@ function assertNodeNames(nodes) {
42
92
  case "signal":
43
93
  assertIdent("step name", node.name);
44
94
  break;
95
+ case "timer":
96
+ assertIdent("step name", node.name);
97
+ break;
45
98
  case "task":
46
99
  assertIdent("step name", node.name);
47
100
  if (node.jobType !== undefined)
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nanobpm/workflow",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/jwulf/nano-ide.git",
9
+ "url": "git+https://github.com/jwulf/nano-ide.git",
10
10
  "directory": "packages/workflow"
11
11
  },
12
12
  "main": "./dist/index.js",
@@ -28,11 +28,11 @@
28
28
  },
29
29
  "sideEffects": false,
30
30
  "dependencies": {
31
- "@nanobpm/nano-sdk": "^1.2.0"
31
+ "@nanobpm/nano-sdk": "^1.2.3"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsc -p tsconfig.json",
35
- "typecheck": "tsc -p tsconfig.json --noEmit",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.types.json",
36
36
  "test": "npm run build && npm run test:ci",
37
37
  "test:ci": "node --experimental-strip-types --test test/*.test.ts",
38
38
  "test:integration": "node --experimental-strip-types --test test/integration.test.ts",