@bpmnkit/engine 0.1.7

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 ADDED
@@ -0,0 +1,130 @@
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/bpmn-sdk/monorepo/main/doc/logos/logo-2-gateway.svg" width="72" height="72" alt="BPMN Kit logo">
3
+ <h1>@bpmnkit/engine</h1>
4
+ <p>Lightweight BPMN 2.0 process execution engine for browsers and Node.js — zero dependencies</p>
5
+
6
+ [![npm](https://img.shields.io/npm/v/@bpmnkit/engine?style=flat-square&color=6244d7)](https://www.npmjs.com/package/@bpmnkit/engine)
7
+ [![license](https://img.shields.io/npm/l/@bpmnkit/engine?style=flat-square)](https://github.com/bpmnkit/monorepo/blob/main/LICENSE)
8
+ [![typescript](https://img.shields.io/badge/TypeScript-strict-6244d7?style=flat-square&logo=typescript&logoColor=white)](https://github.com/bpmnkit/monorepo)
9
+
10
+ [Documentation](https://bpmn-sdk-docs.pages.dev) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/engine/CHANGELOG.md)
11
+ </div>
12
+
13
+ ---
14
+
15
+ ## Overview
16
+
17
+ `@bpmnkit/engine` simulates BPMN 2.0 process execution. Deploy a diagram, start instances, track active elements, evaluate DMN decisions, and step through execution — all without a Camunda cluster.
18
+
19
+ Perfect for: workflow testing, visual debugging, interactive demos, offline simulation, and process-driven UI flows.
20
+
21
+ ## Features
22
+
23
+ - **Full control flow** — exclusive, parallel, inclusive, event-based, complex gateways
24
+ - **Variable scopes** — hierarchical scope chain; FEEL expression evaluation for conditions/mappings
25
+ - **All event types** — message, signal, timer (ISO 8601 duration/date/cycle), error, escalation, compensation
26
+ - **Boundary events** — interrupting and non-interrupting error, timer, compensation
27
+ - **Sub-processes** — embedded, call activity (process invocation by ID)
28
+ - **DMN decisions** — inline decision table evaluation via `@bpmnkit/feel`
29
+ - **Job workers** — register handlers for service tasks by job type
30
+ - **Step-by-step** — `beforeComplete` hook pauses between elements for debugging UIs
31
+ - **Zero dependencies** — browser + Node.js, no server required
32
+
33
+ ## Installation
34
+
35
+ ```sh
36
+ npm install @bpmnkit/engine
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```typescript
42
+ import { Engine } from "@bpmnkit/engine"
43
+
44
+ const engine = new Engine()
45
+
46
+ // Deploy a BPMN process
47
+ engine.deploy({ bpmn: xml })
48
+
49
+ // Register job workers
50
+ engine.registerJobWorker("payment-service", async (job) => {
51
+ const result = await processPayment(job.variables)
52
+ return { success: result.ok }
53
+ })
54
+
55
+ // Start an instance
56
+ const instance = engine.start("order-process", {
57
+ orderId: "ORD-001",
58
+ amount: 99.99,
59
+ })
60
+
61
+ // Track execution
62
+ instance.onChange((state) => {
63
+ console.log("Active:", state.activeElements)
64
+ console.log("Vars:", state.variables_snapshot)
65
+ })
66
+
67
+ // Wait for completion
68
+ await new Promise((resolve) => {
69
+ instance.onChange((state) => {
70
+ if (state.state === "completed" || state.state === "terminated") resolve(undefined)
71
+ })
72
+ })
73
+ ```
74
+
75
+ ## Step-by-step execution
76
+
77
+ ```typescript
78
+ const steps: Array<() => void> = []
79
+
80
+ const instance = engine.start("my-process", {}, {
81
+ beforeComplete: (elementId) =>
82
+ new Promise((resolve) => {
83
+ console.log("Paused at:", elementId)
84
+ steps.push(resolve) // advance by calling steps.pop()()
85
+ }),
86
+ })
87
+ ```
88
+
89
+ ## API Reference
90
+
91
+ ### `Engine`
92
+
93
+ | Method | Description |
94
+ |--------|-------------|
95
+ | `deploy({ bpmn, forms?, decisions? })` | Register BPMN (+ optional DMN/form assets) |
96
+ | `start(processId, variables?, options?)` | Start a new instance; returns `ProcessInstance` |
97
+ | `registerJobWorker(type, handler)` | Handle service tasks with a given job type |
98
+ | `getDeployedProcesses()` | List all deployed process IDs |
99
+
100
+ ### `ProcessInstance`
101
+
102
+ | Member | Description |
103
+ |--------|-------------|
104
+ | `state` | `"running" \| "completed" \| "terminated" \| "failed"` |
105
+ | `activeElements` | IDs of currently active flow nodes |
106
+ | `variables_snapshot` | Flat snapshot of current variable scope |
107
+ | `onChange(cb)` | Subscribe to state changes |
108
+ | `cancel()` | Terminate the instance |
109
+ | `deliverMessage(name, variables?)` | Correlate a message catch event |
110
+ | `beforeComplete?` | Optional step hook (set after `start()`) |
111
+
112
+ ---
113
+
114
+ ## Related Packages
115
+
116
+ | Package | Description |
117
+ |---------|-------------|
118
+ | [`@bpmnkit/core`](https://www.npmjs.com/package/@bpmnkit/core) | BPMN/DMN/Form parser, builder, layout engine |
119
+ | [`@bpmnkit/canvas`](https://www.npmjs.com/package/@bpmnkit/canvas) | Zero-dependency SVG BPMN viewer |
120
+ | [`@bpmnkit/editor`](https://www.npmjs.com/package/@bpmnkit/editor) | Full-featured interactive BPMN editor |
121
+ | [`@bpmnkit/feel`](https://www.npmjs.com/package/@bpmnkit/feel) | FEEL expression language parser & evaluator |
122
+ | [`@bpmnkit/plugins`](https://www.npmjs.com/package/@bpmnkit/plugins) | 22 composable canvas plugins |
123
+ | [`@bpmnkit/api`](https://www.npmjs.com/package/@bpmnkit/api) | Camunda 8 REST API TypeScript client |
124
+ | [`@bpmnkit/ascii`](https://www.npmjs.com/package/@bpmnkit/ascii) | Render BPMN diagrams as Unicode ASCII art |
125
+ | [`@bpmnkit/profiles`](https://www.npmjs.com/package/@bpmnkit/profiles) | Shared auth, profile storage, and client factories for CLI & proxy |
126
+ | [`@bpmnkit/operate`](https://www.npmjs.com/package/@bpmnkit/operate) | Monitoring & operations frontend for Camunda clusters |
127
+
128
+ ## License
129
+
130
+ [MIT](https://github.com/bpmnkit/monorepo/blob/main/LICENSE) © bpmn-sdk
package/dist/dmn.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { DmnDecision } from "@bpmnkit/core";
2
+ /**
3
+ * Evaluate a DMN decision table against the provided variables.
4
+ * Returns the result per the table's hit policy.
5
+ */
6
+ export declare function evaluateDecision(decision: DmnDecision, vars: Record<string, unknown>): unknown;
7
+ //# sourceMappingURL=dmn.d.ts.map
package/dist/dmn.js ADDED
@@ -0,0 +1,115 @@
1
+ import { evaluate, evaluateUnaryTests, parseExpression, parseUnaryTests } from "@bpmnkit/feel";
2
+ /**
3
+ * Evaluate a DMN decision table against the provided variables.
4
+ * Returns the result per the table's hit policy.
5
+ */
6
+ export function evaluateDecision(decision, vars) {
7
+ const table = decision.decisionTable;
8
+ if (table === undefined)
9
+ return null;
10
+ const hitPolicy = table.hitPolicy ?? "UNIQUE";
11
+ const matchedOutputs = [];
12
+ for (const rule of table.rules) {
13
+ if (ruleMatches(table.inputs, rule.inputEntries, vars)) {
14
+ const output = {};
15
+ for (let i = 0; i < table.outputs.length; i++) {
16
+ const col = table.outputs[i];
17
+ const entry = rule.outputEntries[i];
18
+ if (col === undefined || entry === undefined)
19
+ continue;
20
+ const colName = col.name ?? col.label ?? col.id;
21
+ output[colName] = evalOutputEntry(entry.text, vars);
22
+ }
23
+ matchedOutputs.push(output);
24
+ if (hitPolicy === "FIRST")
25
+ break;
26
+ }
27
+ }
28
+ return buildResult(hitPolicy, table.aggregation, matchedOutputs, table.outputs.length);
29
+ }
30
+ function ruleMatches(inputs, inputEntries, vars) {
31
+ const feelVars = vars;
32
+ for (let i = 0; i < inputs.length; i++) {
33
+ const col = inputs[i];
34
+ const entry = inputEntries[i];
35
+ if (col === undefined || entry === undefined)
36
+ continue;
37
+ if (entry.text.trim() === "")
38
+ continue; // empty = "any"
39
+ const inputExpr = col.inputExpression.text ?? "";
40
+ const inputValue = inputExpr.trim() === "" ? null : evalExpression(inputExpr, vars);
41
+ const parsed = parseUnaryTests(entry.text);
42
+ if (parsed.ast === null)
43
+ continue;
44
+ if (!evaluateUnaryTests(parsed.ast, inputValue, { vars: feelVars }))
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ function evalExpression(expr, vars) {
50
+ const parsed = parseExpression(expr.trim());
51
+ if (parsed.ast === null)
52
+ return undefined;
53
+ return evaluate(parsed.ast, { vars: vars });
54
+ }
55
+ function evalOutputEntry(text, vars) {
56
+ if (text.trim() === "")
57
+ return null;
58
+ return evalExpression(text, vars);
59
+ }
60
+ function buildResult(hitPolicy, aggregation, rows, outputCount) {
61
+ if (rows.length === 0)
62
+ return null;
63
+ const singleOutput = outputCount === 1;
64
+ switch (hitPolicy) {
65
+ case "UNIQUE":
66
+ case "FIRST":
67
+ case "ANY": {
68
+ const row = rows[0];
69
+ if (row === undefined)
70
+ return null;
71
+ return singleOutput ? firstValue(row) : row;
72
+ }
73
+ case "RULE ORDER":
74
+ case "OUTPUT ORDER":
75
+ case "PRIORITY": {
76
+ if (singleOutput)
77
+ return rows.map(firstValue);
78
+ return rows;
79
+ }
80
+ case "COLLECT": {
81
+ if (aggregation === undefined) {
82
+ if (singleOutput)
83
+ return rows.map(firstValue);
84
+ return rows;
85
+ }
86
+ const values = rows.map(firstValue).filter((v) => typeof v === "number");
87
+ return aggregate(aggregation, values);
88
+ }
89
+ default:
90
+ return null;
91
+ }
92
+ }
93
+ function firstValue(row) {
94
+ for (const k of Object.keys(row)) {
95
+ return row[k];
96
+ }
97
+ return null;
98
+ }
99
+ function aggregate(op, values) {
100
+ if (values.length === 0)
101
+ return null;
102
+ switch (op) {
103
+ case "SUM":
104
+ return values.reduce((a, b) => a + b, 0);
105
+ case "MIN":
106
+ return Math.min(...values);
107
+ case "MAX":
108
+ return Math.max(...values);
109
+ case "COUNT":
110
+ return values.length;
111
+ default:
112
+ return null;
113
+ }
114
+ }
115
+ //# sourceMappingURL=dmn.js.map
@@ -0,0 +1,36 @@
1
+ import type { BpmnDefinitions, DmnDefinitions, FormDefinition } from "@bpmnkit/core";
2
+ import { ProcessInstance } from "./instance.js";
3
+ import type { JobHandler } from "./types.js";
4
+ /** Options for {@link Engine.start}. */
5
+ export interface StartOptions {
6
+ /**
7
+ * Hook called just before each element completes. Return a Promise to pause
8
+ * execution at that point — useful for step-by-step simulation.
9
+ */
10
+ beforeComplete?: (elementId: string) => Promise<void>;
11
+ }
12
+ export declare class Engine {
13
+ private readonly processes;
14
+ private readonly decisions;
15
+ private readonly forms;
16
+ private readonly workers;
17
+ /**
18
+ * Deploy BPMN processes, DMN decisions, and form definitions.
19
+ * Calling deploy multiple times merges into the registry.
20
+ */
21
+ deploy(d: {
22
+ bpmn?: BpmnDefinitions | BpmnDefinitions[];
23
+ forms?: FormDefinition | FormDefinition[];
24
+ decisions?: DmnDefinitions | DmnDefinitions[];
25
+ }): void;
26
+ /** Start a new process instance. Throws if processId is not deployed. */
27
+ start(processId: string, variables?: Record<string, unknown>, options?: StartOptions): ProcessInstance;
28
+ /**
29
+ * Register a job worker for a given task type.
30
+ * Returns an unsubscribe function.
31
+ */
32
+ registerJobWorker(type: string, handler: JobHandler): () => void;
33
+ /** Return all deployed process IDs. */
34
+ getDeployedProcesses(): string[];
35
+ }
36
+ //# sourceMappingURL=engine.d.ts.map
package/dist/engine.js ADDED
@@ -0,0 +1,66 @@
1
+ import { ProcessInstance } from "./instance.js";
2
+ export class Engine {
3
+ processes = new Map();
4
+ decisions = new Map();
5
+ forms = new Map();
6
+ workers = new Map();
7
+ /**
8
+ * Deploy BPMN processes, DMN decisions, and form definitions.
9
+ * Calling deploy multiple times merges into the registry.
10
+ */
11
+ deploy(d) {
12
+ if (d.bpmn !== undefined) {
13
+ const defs = Array.isArray(d.bpmn) ? d.bpmn : [d.bpmn];
14
+ for (const def of defs) {
15
+ for (const process of def.processes) {
16
+ this.processes.set(process.id, process);
17
+ }
18
+ }
19
+ }
20
+ if (d.decisions !== undefined) {
21
+ const defs = Array.isArray(d.decisions) ? d.decisions : [d.decisions];
22
+ for (const def of defs) {
23
+ for (const decision of def.decisions) {
24
+ this.decisions.set(decision.id, decision);
25
+ }
26
+ }
27
+ }
28
+ if (d.forms !== undefined) {
29
+ const defs = Array.isArray(d.forms) ? d.forms : [d.forms];
30
+ for (const form of defs) {
31
+ const id = form.id;
32
+ if (id !== undefined)
33
+ this.forms.set(id, form);
34
+ }
35
+ }
36
+ }
37
+ /** Start a new process instance. Throws if processId is not deployed. */
38
+ start(processId, variables, options) {
39
+ const process = this.processes.get(processId);
40
+ if (process === undefined) {
41
+ throw new Error(`Process "${processId}" is not deployed`);
42
+ }
43
+ const instance = new ProcessInstance(process, this.decisions, this.forms, this.workers, variables ?? {});
44
+ if (options?.beforeComplete !== undefined) {
45
+ instance.beforeComplete = options.beforeComplete;
46
+ }
47
+ instance.start();
48
+ return instance;
49
+ }
50
+ /**
51
+ * Register a job worker for a given task type.
52
+ * Returns an unsubscribe function.
53
+ */
54
+ registerJobWorker(type, handler) {
55
+ this.workers.set(type, handler);
56
+ return () => {
57
+ if (this.workers.get(type) === handler)
58
+ this.workers.delete(type);
59
+ };
60
+ }
61
+ /** Return all deployed process IDs. */
62
+ getDeployedProcesses() {
63
+ return [...this.processes.keys()];
64
+ }
65
+ }
66
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1,10 @@
1
+ export { Engine } from "./engine.js";
2
+ export type { StartOptions } from "./engine.js";
3
+ export { ProcessInstance } from "./instance.js";
4
+ export type { ProcessEvent, Job, JobHandler } from "./types.js";
5
+ export { VariableStore } from "./variables.js";
6
+ export { evaluateDecision } from "./dmn.js";
7
+ export { scheduleTimer, parseDurationMs } from "./timers.js";
8
+ export { parseZeebeExt } from "./zeebe.js";
9
+ export type { ParsedZeebeExt } from "./zeebe.js";
10
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { Engine } from "./engine.js";
2
+ export { ProcessInstance } from "./instance.js";
3
+ export { VariableStore } from "./variables.js";
4
+ export { evaluateDecision } from "./dmn.js";
5
+ export { scheduleTimer, parseDurationMs } from "./timers.js";
6
+ export { parseZeebeExt } from "./zeebe.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,72 @@
1
+ import type { BpmnProcess, DmnDecision, FormDefinition } from "@bpmnkit/core";
2
+ import type { JobHandler, ProcessEvent } from "./types.js";
3
+ type InstanceState = "active" | "completed" | "terminated" | "failed";
4
+ export declare class ProcessInstance {
5
+ readonly id: string;
6
+ readonly processId: string;
7
+ private _state;
8
+ private _error;
9
+ /** tokenId → Token */
10
+ private readonly allTokens;
11
+ /** Scope stack: rootScopeId + any active sub-process scopes */
12
+ private readonly scopes;
13
+ /** Message correlation: messageName → resolve callback */
14
+ private readonly messageSubscriptions;
15
+ /** Timer cancel functions keyed by tokenId */
16
+ private readonly timerCancels;
17
+ /** Activation count per elementId — used to detect infinite loops. */
18
+ private readonly activationCount;
19
+ private static readonly MAX_ACTIVATIONS;
20
+ /** Active boundary timer cancels, keyed by elementId */
21
+ private readonly boundaryTimerCancels;
22
+ private readonly variables;
23
+ private readonly rootScopeId;
24
+ private readonly listeners;
25
+ private readonly decisions;
26
+ private readonly forms;
27
+ private readonly jobWorkers;
28
+ /**
29
+ * Optional hook called just before an element completes (token moves on).
30
+ * Returning a Promise lets the caller pause execution — useful for
31
+ * step-by-step simulation. Set via {@link Engine.start} options.
32
+ */
33
+ beforeComplete?: (elementId: string) => Promise<void>;
34
+ constructor(process: BpmnProcess, decisions: Map<string, DmnDecision>, forms: Map<string, FormDefinition>, jobWorkers: Map<string, JobHandler>, initialVars: Record<string, unknown>);
35
+ get state(): InstanceState;
36
+ get error(): string | undefined;
37
+ get activeElements(): string[];
38
+ get variables_snapshot(): Record<string, unknown>;
39
+ onChange(callback: (event: ProcessEvent) => void): () => void;
40
+ cancel(): void;
41
+ /** Kick off execution. Called by Engine after construction. */
42
+ start(): void;
43
+ /** Deliver a message to a waiting element. */
44
+ deliverMessage(messageName: string): void;
45
+ private buildScopeCtx;
46
+ private createToken;
47
+ private removeToken;
48
+ /** elementId → set of incomingFlowIds received */
49
+ private readonly joins;
50
+ private activate;
51
+ private dispatch;
52
+ private handleEndEvent;
53
+ private handleJobTask;
54
+ private handleScriptTask;
55
+ private handleBusinessRuleTask;
56
+ private handleExclusiveGateway;
57
+ private handleInclusiveGateway;
58
+ private handleIntermediateCatchEvent;
59
+ private handleSubProcess;
60
+ private scheduleBoundaryTimers;
61
+ private cancelBoundaryTimers;
62
+ private propagateError;
63
+ private complete;
64
+ private getOutgoingFlows;
65
+ private finishProcess;
66
+ private cancelAllTimers;
67
+ private evalFeel;
68
+ private evalCondition;
69
+ private emit;
70
+ }
71
+ export {};
72
+ //# sourceMappingURL=instance.d.ts.map