diff --git a/.github/workflows/build-windows.yaml b/.github/workflows/build-windows.yaml new file mode 100644 index 0000000..b5eebb1 --- /dev/null +++ b/.github/workflows/build-windows.yaml @@ -0,0 +1,20 @@ +name: Build (Windows) + +on: + pull_request: + push: + branches: + - master + +jobs: + build: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Use Node.js from .nvmrc + uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + - run: npm i + - run: npx mocha diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 04dc54c..3dc7cf4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,12 +11,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18, 20, 22, 24, latest] + node-version: [20, 22, 24, latest] steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v7 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: ${{ matrix.node-version }} - run: npm i diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a9dc6c5..f84d1c7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,9 +13,9 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: '24' registry-url: 'https://registry.npmjs.org' diff --git a/.gitignore b/.gitignore index 4e2e76a..a58ea97 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,7 @@ config/_revision package-lock.json *.log + +# Claude Code +CLAUDE.md +.claude diff --git a/.mocharc.json b/.mocharc.json index c6c2062..767e288 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -3,5 +3,6 @@ "reporter": "spec", "timeout": 1000, "ui": "mocha-cakes-2", - "require": ["chai/register-expect.js"] + "spec": ["test/**/*-feature.js", "test/**/*-test.js"], + "require": ["chai/register-expect.js", "./test/helpers/setup.js"] } diff --git a/.nvmrc b/.nvmrc index 3c03207..209e3ef 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +20 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6f2838e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,64 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +`.nvmrc` pins Node 20 (the `toc` bin needs >=20); `engines.node` is `>=18`. The test reporter `@bonniernews/hot-bev` and `mocha-cakes-2` UI handle parallel execution and Gherkin-style Features. + +- `npm test` — runs `mocha -p` with the hot-bev reporter (parallel); the Windows CI build runs plain `npx mocha` (no `-p`, worker pools misbehave there). `posttest` then runs `lint`, `toc` (regenerates doc TOCs), `dist`, and `test:md`. +- `npm run lint` — eslint (cached) + prettier check. +- `npm run cov:html` / `npm run test:lcov` — coverage variants via c8. +- `npm run toc` — regenerates the table of contents in `README.md` and `docs/*.md` with [`@0dep/toc`](https://0dep.se/toc/). It rewrites the block between `` and `` and lists every heading below the start marker, so the markers sit under the doc title to keep it out of the toc. Duplicate anchors are disambiguated GitHub-style (`#getstate`, `#getstate-1`). Output is prettier-compatible. +- `npm run test:md` — runs `texample` against `docs/API.md`, `docs/Examples.md`, `docs/Upgrade.md`. Runs as the last step of `posttest`, and standalone when doc examples change. The examples that mock HTTP with `nock` currently fail with `StatusError: Not Found` on this branch, independent of doc edits. +- `npm run dist` — rollup builds `src/index.js` → `lib/index.cjs`. The footer trick `module.exports = Object.assign(exports.default, exports)` is what lets the CJS bundle's `require('bpmn-engine')` return the `Engine` constructor _and_ expose named exports — don't remove it. +- Single test file: `npx mocha test/Engine-test.js`. Single test: append `--grep ""`. Default mocha timeout is 1000ms (see `.mocharc.json`); feature tests under `test/feature/` use the `mocha-cakes-2` BDD UI (`Feature`/`Scenario`/`Given`/`When`/`Then`). + +## Architecture + +The `Engine` is a thin orchestrator on top of four heavyweights — every file in `src/` is glue: + +``` +bpmn-moddle (parse XML) + │ + ▼ +moddle-context-serializer ──► persistable JSON state (serialize / deserialize) + │ + ▼ +bpmn-elements (behaviour functions: Process, Task, *EventDefinition, …) + │ + ▼ +smqp (peer dep) ── message broker driving execution +``` + +`src/index.js` (~630 lines) wires these together: + +1. **Construction** — `Engine(options)` builds a `TypeResolver` from `bpmn-elements` + user `elements` overrides, creates an `Elements.Environment`, and instantiates a `smqp` `Broker` for engine-level events. Defaults can be passed via `Logger`, `scripts` (defaults to `JavaScripts.js` — Node `vm`-based inline script handler), `extensions`, etc. +2. **Source loading** — `addSource({ source | sourceContext })` queues sources. `source` is raw BPMN XML; `sourceContext` is a serializer output (skips XML parse). Both end up as `moddle-context-serializer` `SerializableContext`s. +3. **Execution** — `execute()` resolves all pending sources to `Definition`s (from `bpmn-elements`), starts them via the broker, and listens to broker events to surface activity status (`executing` / `timer` / `wait` / `idle`). +4. **State** — `getState()` walks each definition and pulls its serializable state; `recover(state)` reconstructs definitions; `resume()` restarts execution. As of v25, **a running engine cannot be recovered** — must be stopped first or use a fresh `Engine` instance (see CHANGELOG). + +### Conventions worth knowing + +- **Symbol-keyed private state.** All instance internals on `Engine`/execution use `Symbol.for('engine')`, `Symbol.for('environment')`, etc. This is the project's privacy convention — don't replace with `#fields` (would break `Object.assign`-based extension and the recover/resume state walking). +- **`getOptionsAndCallback.js`** — small helper that lets every public method accept either `(options, callback)` or `(callback)` style. Preserves the legacy callback-friendly API while the implementation returns Promises. +- **Extensions live under `src/extensions/`.** `ProcessOutputDataObject.js` is the canonical example — extensions plug into `bpmn-elements` via the engine's `extensions` option and are typically called for specific element types. The wider BPMN ecosystem — docs, guides, and additional extensions for this stack — is published at [0dep.se](https://0dep.se); check there before writing a new extension from scratch. +- **Test fixtures under `test/resources/`** are real `.bpmn` XML files plus a few `.json` extensions. `test/resources/JsExtension.js` and `test/resources/js-bpmn-moddle.json` define a custom moddle extension namespace used across multiple feature tests. +- **`mocha-cakes-2` ≠ standard mocha.** Files under `test/feature/` use Gherkin-flavored globals (`Feature`, `Scenario`, `Given`, `When`, `Then`, `And`, `But`). They're ESLint-allowed via the eslint config's globals. + +## Build & publishing + +- `type: module` in `package.json`; ESM is `src/index.js`, CJS is `lib/index.cjs` (rollup), types are hand-maintained at `types/index.d.ts` (the published `declare module 'bpmn-engine'` surface, referenced by `src` JSDoc via package self-reference `import('bpmn-engine')`). +- `prepack` runs `dist`, so `npm publish` always ships a fresh CJS bundle. `lib/index.cjs` is tracked in git — regenerate (`npm run dist`) and commit alongside source changes that affect the public API. +- Tests import via package self-reference (`import { Engine } from 'bpmn-engine'`), not `'../src/index.js'`. This exercises the same module resolution consumers use. + +## Dependency notes + +- **`moddle-context-serializer`** — this engine is the primary consumer that calls `.serialize()` / `deserialize()` for state persistence. The serialized JSON is a _runtime persistence format_ for live engines, so changes to it break upgrade-path for deployed engines, not just code. Keep that in mind when bumping the dep — even a "harmless" field rename in the serializer's output ripples here. +- **`bpmn-elements`** — supplies the behaviour function registry passed to `TypeResolver`. Adding new element types means adding an entry there first. +- **`smqp`** is a `peerDependency` (`>=9`) — same-maintainer message broker that drives both engine-level and definition-level event flow. Consumers install it directly. Coordinated breaking changes are feasible since the same hand maintains it. +- **`bpmn-moddle`** is a regular dep (currently `^9.0.4`). v10 changed default → named export but `src/index.js` still does `import BpmnModdle from 'bpmn-moddle'` — bumping to v10 will break the import. + +## Downstream consumers + +This engine is the user-facing entry point of the BPMN library stack — most application code that runs BPMN processes uses `bpmn-engine` directly, not the lower-level `bpmn-elements` / `moddle-context-serializer`. State persistence shape (the JSON returned by `getState()`) is therefore the most consumer-sensitive surface — schema changes break stored process state on upgrade. diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e7702..a3a990b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,39 @@ # Changelog -## [25.0.1] - 2025-11-14 +## v26.0.6 - 2026-09-14 + +Runs on [`bpmn-elements@18`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) that no longer discards sequence flows. Most diagrams run unchanged; see the [upgrade guide](/docs/Upgrade.md) for migration details. + +### Breaking + +- major update [`bpmn-elements@18`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) — non-selected gateway flows are no longer discarded, so `flow.discard` and `flow.looped` are never emitted +- update to [`moddle-context-serializer@6`](https://github.com/paed01/moddle-context-serializer/blob/master/CHANGELOG.md) +- declare `bpmn-elements`, `bpmn-moddle`, `moddle-context-serializer`, `debug`, and `smqp` as peer dependencies (install and pin them yourself), see [peer dependencies](/README.md#peer-dependencies) +- require [`smqp@>=15`](https://github.com/paed01/smqp/blob/default/CHANGELOG.md) that has straightened out routing quirks +- support both `bpmn-moddle` 9 and 10 — see [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) + +### Types + +- ship hand-maintained TypeScript declarations and type-check the test suite with them +- `typeResolver` option is a type resolver extender function, `JavaScripts` is callable without `new`, and `getScript` may return undefined +- `Execution` requires `new` +- `BpmnSequenceFlowEvent` is `flow.take` only +- remove `wait` from `BpmnEngineEvent` — the engine itself never emits it; `waitFor` accepts any event name for custom emits +- add missing `activity.timer`, `activity.timeout`, `activity.signal`, `activity.catch`, `activity.discard`, and `activity.cancel` to `BpmnActivityEvent` +- add `BpmnProcessEvent`, `BpmnDefinitionEvent`, and `BpmnListenerEvent` types and type `IListenerEmitter.emit` with `(eventName, elementApi, execution)` + +### Other + +- document the above events in the API reference +- generate table of contents with [`@0dep/toc`](https://0dep.se/toc/) +- run the Windows build as a GitHub action instead of AppVeyor, Node version pinned by `.nvmrc` + +## v25.0.1 - 2025-11-14 - provenance release to get green badge next to npm package version - bump all provenance updated packages -## [25.0.0] - 2025-03-17 +## v25.0.0 - 2025-03-17 It has been possible to recover a running engine. The execution was overwritten and all references to timers etc was lost. This stops with this version. Either stop the the execution or wait for it to end if the engine should be re-used. It is highly recommended to initiate a new Engine when recovering from state. @@ -17,44 +45,44 @@ It has been possible to recover a running engine. The execution was overwritten - minor update of moddle-context-serializer brings some performance improvement -## [24.0.1] - 2025-03-14 +## v24.0.1 - 2025-03-14 - fix recover engine with options not keeping scripts and logger from when initiated > NB! Next major versions will not accept recovering a running engine. -## [24.0.0] - 2025-02-08 +## v24.0.0 - 2025-02-08 - major update [`bpmn-elements@17`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md), something about mitigating weird formatting behaviour -## [23.0.2] - 2024-10-22 +## v23.0.2 - 2024-10-22 - move `smqp` to peerDependencies since it's included in `bpmn-elements` - patch [`bpmn-elements@16.2.1`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) - use optional chaining and remove futile object creations -## [23.0.1] - 2024-09-08 +## v23.0.1 - 2024-09-08 - patch [`bpmn-elements@16.1.0`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) - fix AppVeyor build badge -## [23.0.0] - 2024-08-08 +## v23.0.0 - 2024-08-08 ### Breaking - changed `ConditionalEventDefinition` behaviour in [`bpmn-elements@16`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) -## [22.0.2] - 2024-07-10 +## v22.0.2 - 2024-07-10 - patch [`bpmn-elements@15.0.3`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) -## [22.0.1] - 2024-06-19 +## v22.0.1 - 2024-06-19 - for a number of years the `extendFn` option has been accepted, but never passed to moddle-context-serializer. Now it is - fix type declaration for options `extendFn` and `TypeResolver` - type declare that a generic listener instance with an emit function works just as well as a full blown new EventEmitter -## [22.0.0] - 2024-06-14 +## v22.0.0 - 2024-06-14 Performance tweaks. @@ -66,11 +94,11 @@ Performance tweaks. - drop support for node 14 and 16 - JavaScripts.Script property scripts is now a Map -## [21.1.0] - 2024-05-17 +## v21.1.0 - 2024-05-17 - minor bump [`bpmn-elements@14.1.0`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md), addresses issue #180 -## [21.0.0] - 2024-05-04 +## v21.0.0 - 2024-05-04 Stop execution if invalid time duration, cycle, or date is encountered. @@ -78,36 +106,36 @@ Stop execution if invalid time duration, cycle, or date is encountered. - invalid `TimerEventDefinition` timer type value stops execution according to [`bpmn-elements@14`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md). Old behaviour can be achieved by using bpmn-elements@13 -## 20.0.2 +## v20.0.2 - patch away package prettier from smqp -## 20.0.1 +## v20.0.1 - hoist current definition environment output to engine environment output if run fails - Major bump `bpmn-moddle@9` - Minor bump [`moddle-context-serializer@4.2`](https://github.com/paed01/moddle-context-serializer/blob/master/CHANGELOG.md) - Minor bump [`bpmn-elements@13.2.0`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) -## 20.0.0 +## v20.0.0 - turn into esm with exports for node - build with node >= 18, should still work with earlier versions but proceed with caution and make tests - remove eslint formatting rules in favor of prettier, touched basically all files but now it is "pretty" - prototype `ProcessOutputDataObject` and make properties id and type readonly -## 19.0.1 +## v19.0.1 - Patch [`bpmn-elements`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) -## 19.0.0 +## v19.0.0 Upgrade is recommended since nasty evergroving state size is fixed. - Major bump [`bpmn-elements@13`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) - remove enumerable flag on prototype properties -## 18.0.0 +## v18.0.0 Only breaking if multi-instance sub-process executions are inspected after sub-process run is completed. Picture a multi-instance sequential sub-process with a cardinality of 100. One hundred items in a list occupies some memory. That will not stand. Consequently, they are now removed when iteration completes and eventually collected by gc. diff --git a/README.md b/README.md index 323f4c7..c7e6739 100644 --- a/README.md +++ b/README.md @@ -2,27 +2,64 @@ [![Project Status: Active - The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) -[![Build](https://github.com/paed01/bpmn-engine/actions/workflows/build.yaml/badge.svg)](https://github.com/paed01/bpmn-engine/actions/workflows/build.yaml)[![Build status](https://ci.appveyor.com/api/projects/status/670n39fivq1g3nu5/branch/master?svg=true)](https://ci.appveyor.com/project/paed01/bpmn-engine/branch/master)[![Coverage Status](https://coveralls.io/repos/github/paed01/bpmn-engine/badge.svg?branch=master)](https://coveralls.io/github/paed01/bpmn-engine?branch=master) +[![Build](https://github.com/paed01/bpmn-engine/actions/workflows/build.yaml/badge.svg)](https://github.com/paed01/bpmn-engine/actions/workflows/build.yaml)[![Build (Windows)](https://github.com/paed01/bpmn-engine/actions/workflows/build-windows.yaml/badge.svg)](https://github.com/paed01/bpmn-engine/actions/workflows/build-windows.yaml)[![Coverage Status](https://coveralls.io/repos/github/paed01/bpmn-engine/badge.svg?branch=master)](https://coveralls.io/github/paed01/bpmn-engine?branch=master) # Introduction BPMN 2.0 execution engine. Open source javascript workflow engine. +Documentation: + - [API](/docs/API.md) -- [Changelog](/CHANGELOG.md) - [Examples](/docs/Examples.md) - [Upgrade version](/docs/Upgrade.md) +- [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) +- [Changelog](/CHANGELOG.md) + + + +- [Peer dependencies](#peer-dependencies) - [Supported elements](#supported-elements) +- [Extensions](#extensions) - [Debug](#debug) -- [Example process](#a-pretty-image-of-a-process) +- [A pretty image of a process](#a-pretty-image-of-a-process) - [Acknowledgments](#acknowledgments) + + +# Peer dependencies + +The runtime dependencies are declared as **peer dependencies**, so you install and pin them yourself and the engine reuses your single copy: + +```sh +npm install bpmn-engine bpmn-elements bpmn-moddle moddle-context-serializer debug smqp +``` + +| Peer dependency | Range | Role | +| ---------------------------------------------------------------------------------- | ----------- | ------------------------------------------------- | +| [`bpmn-elements`](https://github.com/paed01/bpmn-elements) | `>=18.0.25` | element behaviour functions | +| [`bpmn-moddle`](https://github.com/bpmn-io/bpmn-moddle) | `>=9` | BPMN XML parser ([9 vs 10](/docs/bpmn-moddle.md)) | +| [`moddle-context-serializer`](https://github.com/paed01/moddle-context-serializer) | `>=6` | persistable source context | +| [`smqp`](https://github.com/paed01/smqp) | `>=15` | message broker driving execution | +| [`debug`](https://github.com/debug-js/debug) | `>=4` | logging | + +`bpmn-moddle` spans a major version on purpose — see [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) for the import change and how persisted state stays compatible across the upgrade. + # Supported elements See [bpmn-elements](https://github.com/paed01/bpmn-elements) for supported elements. The engine only support elements and attributes included in the BPMN 2.0 scheme, but can be extended to understand other schemas and elements. The aim is to, at least, have BPMN 2.0 [core support](https://www.omg.org/bpmn/Samples/Elements/Core_BPMN_Elements.htm). +# Extensions + +The engine can be extended to understand other schemas and elements. Docs, guides, and ready-made extensions for the BPMN stack are published at [0dep.se](https://0dep.se) — check there before writing a new extension from scratch. + +Two ready-made extensions: + +- [`@0dep/bpmn-extensions`](https://github.com/zerodep/bpmn-extensions) +- [`@onify/flow-extensions`](https://github.com/onify/flow-extensions) + # Debug This package is shipped with [debug](https://github.com/debug-js/debug) activated with environment variable `DEBUG=bpmn-engine:*`. You can also provide your own logger. diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 4871d7f..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,18 +0,0 @@ -environment: - matrix: - - nodejs_version: '18' - - nodejs_version: '20' - -platform: - - x64 - -install: - - ps: Install-Product node $env:nodejs_version $env:platform - - npm install - -build: off - -test_script: - - node --version - - npm --version - - npm run wintest diff --git a/docs/API.md b/docs/API.md index 68e3d08..94919ed 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,8 +1,4 @@ - - -# 25.0.1 API Reference - - +# API Reference @@ -21,7 +17,7 @@ - [`resume([options, [callback]])`](#resumeoptions-callback) - [Execution API](#execution-api) - [`getActivityById(activityId)`](#getactivitybyidactivityid) - - [`getState()`](#getstate) + - [`getState()`](#getstate-1) - [`signal(message[, options])`](#signalmessage-options) - [`cancelActivity(message)`](#cancelactivitymessage) - [Engine events](#engine-events) @@ -30,7 +26,7 @@ - [Sequence flow events](#sequence-flow-events) - [Expressions](#expressions) - + # Engine @@ -319,7 +315,7 @@ import { EventEmitter } from 'node:events'; import BpmnModdle from 'bpmn-moddle'; import * as elements from 'bpmn-elements'; import { Engine } from 'bpmn-engine'; -import Serializer, { TypeResolver } from 'moddle-context-serializer'; +import { Serializer, TypeResolver } from 'moddle-context-serializer'; const engine = new Engine({ name: 'add source', @@ -413,11 +409,14 @@ Asynchronous function to get state of a running execution. The saved state will include the following content: -- `state`: `running` or `idle` +- `name`: engine name +- `state`: `running`, `idle`, `stopped`, or `error` +- `stopped`: boolean stopped - `engineVersion`: module package version -- `moddleOptions`: Engine moddleOptions +- `environment`: serialized engine environment - `definitions`: List of definitions - `state`: State of definition, `pending`, `running`, or `completed` + - `source`: serialized definition source - `processes`: Object with processes with id as key - `variables`: Execution variables - `services`: Execution services @@ -638,6 +637,12 @@ Each activity and flow emits events when changing state. - `activity.start`: An activity is started - `activity.wait`: The activity is postponed for some reason, e.g. a user task is waiting to be signaled or a message is expected - `wait`: Same as above +- `activity.timer`: A timer was started +- `activity.timeout`: A timer timed out +- `activity.signal`: The activity was signaled +- `activity.catch`: The activity caught a message, signal, error, etc +- `activity.discard`: The activity was discarded +- `activity.cancel`: The activity was cancelled - `activity.end`: An activity has ended successfully - `activity.leave`: The execution left the activity - `activity.stop`: Activity run was stopped @@ -660,8 +665,8 @@ Events are emitted with api with execution properties ## Sequence flow events - `flow.take`: The sequence flow was taken -- `flow.discard`: The sequence flow was discarded -- `flow.looped`: The sequence is looped + +Since [bpmn-elements@18](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) non-selected sequence flows are no longer discarded, hence `flow.discard` and `flow.looped` are no longer emitted. # Expressions diff --git a/docs/Examples.md b/docs/Examples.md index 0912045..9605c7f 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -16,7 +16,7 @@ - [Traverse activities using definition shake](#traverse-activities-using-definition-shake) - [Persist state on events](#persist-state-on-events) - + # Execute @@ -765,7 +765,7 @@ import { fileURLToPath } from 'node:url'; import { Engine } from 'bpmn-engine'; import BpmnModdle from 'bpmn-moddle'; import * as elements from 'bpmn-elements'; -import Serializer, { TypeResolver } from 'moddle-context-serializer'; +import { Serializer, TypeResolver } from 'moddle-context-serializer'; const camunda = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json'); @@ -828,23 +828,23 @@ In this example the state of the execution is published on a message broker. Sub ```js import { randomUUID } from 'node:crypto'; import { createRequire } from 'node:module'; -import { fileURLToPath } = from 'node:url'; +import { fileURLToPath } from 'node:url'; +import { EventEmitter } from 'node:events'; import { Engine } from 'bpmn-engine'; -import { EventEmitter } from 'ndoe:events'; import { publish } from './dbbroker.js'; import { getSourceSync, getAllowedServices, getExtensions } from './utils.js'; -const camundaModdle = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json') +const camunda = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json'); function ignite(executionId, options = {}) { const { name, settings } = options; const listener = new EventEmitter(); listener.on('activity.wait', (_, execution) => { - return publishEvent('bpmn.state.update', {state: execution.getState()}); + return publishEvent('bpmn.state.update', { state: execution.getState() }); }); listener.on('activity.end', (_, execution) => { - return publishEvent('bpmn.state.update', {state: execution.getState()}); + return publishEvent('bpmn.state.update', { state: execution.getState() }); }); listener.on('activity.timer', (api, execution) => { return publishEvent('bpmn.state.expires', { @@ -859,7 +859,7 @@ function ignite(executionId, options = {}) { }); }); - const engine = BpmnEngine({ + const engine = new Engine({ moddleOptions: { camunda, }, @@ -868,13 +868,13 @@ function ignite(executionId, options = {}) { ...settings, executionId, enableDummyService: false, - } + }, }); engine.once('end', () => { publishEvent('bpmn.completed'); }); engine.once('error', (err) => { - publishEvent('bpmn.error', {message: err.message, error: err}); + publishEvent('bpmn.error', { message: err.message, error: err }); }); return { engine, listener }; @@ -888,7 +888,7 @@ function ignite(executionId, options = {}) { } } -const {engine} = ignite(randomUUID(), { +const { engine } = ignite(randomUUID(), { name: 'persisted engine #1', source: getSourceSync('./mother-of-all.bpmn'), services: getAllowedServices(), diff --git a/docs/Upgrade.md b/docs/Upgrade.md index 67c6012..eb3afc5 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -1,5 +1,49 @@ # Upgrade + + +- [v25 → v26](#v25--v26) + - [Peer dependencies](#peer-dependencies) + - [Sequence flows are no longer discarded](#sequence-flows-are-no-longer-discarded) + - [Resuming state saved by v25](#resuming-state-saved-by-v25) + - [Types](#types) +- [< v14](#-v14) + + + +# v25 → v26 + +Version 26 runs on [`bpmn-elements@18`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md), which refactors parallel gateway convergence and removes sequence flow discards altogether. Most diagrams run unchanged, but the behavioural changes are in the elements, not the engine, so read the [bpmn-elements upgrade guide](https://github.com/paed01/bpmn-elements/blob/master/docs/Upgrade.md) first. What follows is what changes from the engine's point of view. + +## Peer dependencies + +`bpmn-elements`, `bpmn-moddle`, `moddle-context-serializer`, `smqp`, and `debug` are peer dependencies. Install and pin them yourself, see [peer dependencies](/README.md#peer-dependencies) for the supported ranges. `smqp@15` or later is required. + +`bpmn-moddle` 9 and 10 are both supported — see [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) for the import change. + +## Sequence flows are no longer discarded + +An untaken branch is left untouched instead of propagating a chain of discards. For an engine host this means: + +- the `flow.discard` and `flow.looped` events are never emitted, so the `listener` and `engine.broker` only see `flow.take`. The `BpmnSequenceFlowEvent` type reflects this. +- `discarded` counters on sequence flows and downstream activities stay at `0` for untaken branches. Track `flow.take` and `activity.end` instead. An activity discarded through the api still increments its own counter. +- multiple start events in a process are mutually exclusive entry points, an `IntermediateCatchEvent` without inbound flows is no longer started by default, and starting activities that are not start events are no longer auto-discarded. Diagrams relying on any of these must be redesigned. + +The details, including converging parallel gateways and shake output, are in the [bpmn-elements upgrade guide](https://github.com/paed01/bpmn-elements/blob/master/docs/Upgrade.md#no-more-flow-discards). + +## Resuming state saved by v25 + +State saved by v25 can be recovered and resumed by v26. `bpmn-elements@18` stamps definition state with a `stateVersion` and migrates older state on resume: start events are reconciled to the mutually exclusive rule and stale discard tokens left on process queues are acked, so they no longer strand process completion. No action is required beyond resuming; saving again stamps the current version. See [resuming state saved by v17](https://github.com/paed01/bpmn-elements/blob/master/docs/Upgrade.md#resuming-state-saved-by-v17). + +## Types + +The engine ships hand-maintained TypeScript declarations. Recompile a TypeScript host against them: + +- `Execution` requires `new` +- `BpmnSequenceFlowEvent` is `flow.take` only, and `wait` is removed from `BpmnEngineEvent` since the engine never emits it +- `BpmnActivityEvent` covers `activity.timer`, `activity.timeout`, `activity.signal`, `activity.catch`, `activity.discard`, and `activity.cancel`, and `BpmnProcessEvent`, `BpmnDefinitionEvent`, and `BpmnListenerEvent` are new +- the `typeResolver` option is a type resolver extender function, `JavaScripts` is callable without `new`, and `getScript` may return undefined + # < v14 Since v14 of the engine output is no longer shared between definition and processes. To upgrade a saved state before version 14 you can run the following script that adds process environment to state. diff --git a/docs/bpmn-moddle.md b/docs/bpmn-moddle.md new file mode 100644 index 0000000..e901fd8 --- /dev/null +++ b/docs/bpmn-moddle.md @@ -0,0 +1,43 @@ +# bpmn-moddle 9 vs 10 + +`bpmn-engine` declares [`bpmn-moddle`](https://github.com/bpmn-io/bpmn-moddle) as a **peer dependency** with the range `>=9`. You install it yourself and you are free to run the engine on either the 9.x or 10.x line: + +```sh +npm install bpmn-engine bpmn-moddle smqp +``` + +The engine uses `bpmn-moddle` only to parse BPMN XML into a moddle context (`new BpmnModdle().fromXML(source)`). The moddle context is then handed to [`moddle-context-serializer`](https://github.com/paed01/moddle-context-serializer) to produce the persistable source context. Everything downstream of the parse — execution, `getState()`, `recover()`, `resume()` — is independent of the `bpmn-moddle` version. + +## What changed in 10 + +The output of `fromXML()` is unchanged between 9 and 10 — same `{ rootElement, elementsById, references, warnings }` shape, same element ids and references. The breaking change is purely how the package is consumed: + +| | bpmn-moddle 9 | bpmn-moddle 10 | +| ------------- | -------------------------------------- | ------------------------------------------ | +| Module entry | CommonJS | ESM (`type: module`) | +| Parser export | **default** export | **named** export `BpmnModdle` | +| Import | `import BpmnModdle from 'bpmn-moddle'` | `import { BpmnModdle } from 'bpmn-moddle'` | +| `require()` | `require('bpmn-moddle')` | not available (ESM only) | + +```js +// bpmn-moddle 9 +import BpmnModdle from 'bpmn-moddle'; + +// bpmn-moddle 10 +import { BpmnModdle } from 'bpmn-moddle'; +``` + +`bpmn-engine` resolves whichever export shape the installed version provides, so the `source` option — where the engine parses the XML for you — works on both lines. No code changes are needed to move between 9 and 10. + +One caveat for CommonJS consumers (`require('bpmn-engine')`): bpmn-moddle 10 is ESM-only, so the engine's CJS bundle reaches it through Node's `require(esm)` support — available since Node 20.19 / 22.12. On older Node, stay on bpmn-moddle 9 or migrate to `import`. + +## Backward compatibility of persisted state + +Because the serialized source context and the runtime state are independent of the `bpmn-moddle` version, **state saved by a deployment running bpmn-moddle 9 keeps resuming after an upgrade to bpmn-moddle 10** (and vice versa). `recover()` deserializes the embedded source context — or a source context you re-parse with the new version — without ever calling `bpmn-moddle` itself. + +This is pinned down by the feature test [`test/feature/bpmn-moddle-backward-compatibility-feature.js`](../test/feature/bpmn-moddle-backward-compatibility-feature.js), which: + +1. starts the _mother-of-all_ process from a context parsed with bpmn-moddle **9**, stops it at the first user task wait and saves the state; +2. re-parses the same source with bpmn-moddle **10**, recovers the v9 runtime state against the v10 source context, and resumes it to completion; +3. asserts that the source context serialized via 9 and via 10 are byte-for-byte equal; +4. runs the engine with the raw `source` option in a child process where `bpmn-moddle` resolves to **10**, proving the engine's own parse path handles the named export. diff --git a/eslint.config.js b/eslint.config.js index 9ce23c5..0aaafa0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,7 +10,8 @@ const rules = { 'no-array-constructor': 2, 'no-caller': 2, 'no-catch-shadow': 2, - 'no-console': 1, + 'no-console': 'error', + 'no-duplicate-imports': 'error', 'no-eval': 2, 'no-extend-native': 2, 'no-extra-bind': 2, @@ -43,7 +44,7 @@ const rules = { 'no-undef': 2, 'no-underscore-dangle': 0, 'no-unused-expressions': 2, - 'no-unused-vars': 2, + 'no-unused-vars': ['error', { ignoreRestSiblings: true }], 'no-use-before-define': [ 'error', { @@ -55,6 +56,7 @@ const rules = { ], 'no-var': 2, 'no-with': 2, + 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], 'prefer-const': ['error', { destructuring: 'all' }], 'require-atomic-updates': 0, 'require-await': 2, @@ -74,7 +76,7 @@ export default [ languageOptions: { parserOptions: { sourceType: 'module', - ecmaVersion: 2020, + ecmaVersion: 2025, }, }, rules, @@ -87,6 +89,17 @@ export default [ }, }, }, + { + files: ['scripts/**/*.js'], + languageOptions: { + globals: { + ...globals.node, + }, + }, + rules: { + 'no-process-exit': 0, + }, + }, { files: ['test/**/*.js'], languageOptions: { diff --git a/lib/index.cjs b/lib/index.cjs index f6e49f4..8e04630 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -5,10 +5,10 @@ Object.defineProperty(exports, '__esModule', { value: true }); var node_module = require('node:module'); var node_events = require('node:events'); var node_url = require('node:url'); -var BpmnModdle = require('bpmn-moddle'); +var bpmnModdle = require('bpmn-moddle'); var Elements = require('bpmn-elements'); var smqp = require('smqp'); -var serializer = require('moddle-context-serializer'); +var moddleContextSerializer = require('moddle-context-serializer'); var Debug = require('debug'); var node_vm = require('node:vm'); @@ -30,6 +30,7 @@ function _interopNamespaceDefault(e) { return Object.freeze(n); } +var bpmnModdle__namespace = /*#__PURE__*/_interopNamespaceDefault(bpmnModdle); var Elements__namespace = /*#__PURE__*/_interopNamespaceDefault(Elements); function Logger(scope) { @@ -164,6 +165,9 @@ ProcessOutputDataObject.prototype.write = function writeDataObject(broker, excha const nodeRequire = node_module.createRequire(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))); const { version: engineVersion } = nodeRequire('../package.json'); +// bpmn-moddle@9 exposes the parser as default export, bpmn-moddle@10 as the named export BpmnModdle +const BpmnModdle = bpmnModdle__namespace.BpmnModdle ?? bpmnModdle__namespace.default; + const kEngine = Symbol.for('engine'); const kEnvironment = Symbol.for('environment'); const kExecuting = Symbol.for('executing'); @@ -175,6 +179,10 @@ const kState = Symbol.for('state'); const kStopped = Symbol.for('stopped'); const kTypeResolver = Symbol.for('type resolver'); +/** + * BPMN 2.0 execution engine. + * @param {import('bpmn-engine').BpmnEngineOptions} [options] + */ function Engine(options) { if (!(this instanceof Engine)) return new Engine(options); @@ -188,7 +196,7 @@ function Engine(options) { this.logger = opts.Logger('engine'); - this[kTypeResolver] = serializer.TypeResolver( + this[kTypeResolver] = moddleContextSerializer.TypeResolver( { ...Elements__namespace, ...opts.elements, @@ -260,6 +268,12 @@ Object.defineProperties(Engine.prototype, { }, }); +/** + * Execute the loaded definitions. + * @param {import('bpmn-engine').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {(err: Error, execution?: Execution) => void} [callback] + * @returns {Promise} + */ Engine.prototype.execute = async function execute(...args) { const [executeOptions, callback] = getOptionsAndCallback(...args); try { @@ -273,12 +287,19 @@ Engine.prototype.execute = async function execute(...args) { return execution._execute(executeOptions, callback); }; +/** @returns {Promise} */ Engine.prototype.stop = function stop() { const execution = this.execution; if (!execution) return; return execution.stop(); }; +/** + * Recover engine from saved state. + * @param {import('bpmn-engine').BpmnEngineExecutionState | null} savedState + * @param {import('bpmn-engine').BpmnEngineOptions} [recoverOptions] + * @returns {Engine} + */ Engine.prototype.recover = function recover(savedState, recoverOptions) { if (this[kExecution]?.isRunning) { throw new Error('cannot recover a running engine'); @@ -306,7 +327,7 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { const typeResolver = this[kTypeResolver]; const loadedDefinitions = (this[kLoadedDefinitions] = savedState.definitions.map((dState) => { let source; - if (dState.source) source = serializer.deserialize(JSON.parse(dState.source), typeResolver); + if (dState.source) source = moddleContextSerializer.deserialize(JSON.parse(dState.source), typeResolver); else source = preSources.find((s) => s.id === dState.id); pendingSources.add(source); @@ -324,6 +345,12 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { return this; }; +/** + * Resume execution from a previously recovered state. + * @param {import('bpmn-engine').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {(err: Error, execution?: Execution) => void} [callback] + * @returns {Promise} + */ Engine.prototype.resume = async function resume(...args) { const [resumeOptions, callback] = getOptionsAndCallback(...args); @@ -345,6 +372,10 @@ Engine.prototype.resume = async function resume(...args) { return execution._resume(resumeOptions, callback); }; +/** + * Add a pre-serialized source context to the engine. + * @param {{ sourceContext: import('moddle-context-serializer').SerializableContext }} [options] + */ Engine.prototype.addSource = function addSource(options) { if (!options?.sourceContext) return; const loadedDefinitions = this[kLoadedDefinitions]; @@ -352,16 +383,25 @@ Engine.prototype.addSource = function addSource(options) { this[kPendingSources].add(options.sourceContext); }; +/** + * @param {import('bpmn-engine').BpmnEngineExecuteOptions} [executeOptions] + * @returns {Promise} + */ Engine.prototype.getDefinitions = function getDefinitions(executeOptions) { const loadedDefinitions = this[kLoadedDefinitions]; if (loadedDefinitions?.length) return Promise.resolve(loadedDefinitions); return this._loadDefinitions(executeOptions); }; +/** + * @param {string} id + * @returns {Promise} + */ Engine.prototype.getDefinitionById = async function getDefinitionById(id) { return (await this.getDefinitions()).find((d) => d.id === id); }; +/** @returns {Promise} */ Engine.prototype.getState = async function getState() { const execution = this.execution; if (execution) return execution.getState(); @@ -370,6 +410,11 @@ Engine.prototype.getState = async function getState() { return new Execution(this, definitions, this.options).getState(); }; +/** + * @template R + * @param {import('bpmn-engine').BpmnEngineEvent | string} eventName + * @returns {Promise} + */ Engine.prototype.waitFor = function waitFor(eventName) { const self = this; return new Promise((resolve, reject) => { @@ -387,12 +432,14 @@ Engine.prototype.waitFor = function waitFor(eventName) { }); }; +/** @internal */ Engine.prototype._loadDefinitions = async function loadDefinitions(executeOptions) { const runSources = await Promise.all(this[kPendingSources]); const loadedDefinitions = (this[kLoadedDefinitions] = runSources.map((source) => this._loadDefinition(source, executeOptions))); return loadedDefinitions; }; +/** @internal */ Engine.prototype._loadDefinition = function loadDefinition(serializedContext, executeOptions) { const environment = this.environment; const context = new Elements__namespace.Context( @@ -415,20 +462,29 @@ Engine.prototype._loadDefinition = function loadDefinition(serializedContext, ex return new Elements__namespace.Definition(context); }; +/** @internal */ Engine.prototype._serializeSource = async function serializeSource(source) { const moddleContext = await this._getModdleContext(source); return this._serializeModdleContext(moddleContext); }; +/** @internal */ Engine.prototype._serializeModdleContext = function serializeModdleContext(moddleContext) { - return serializer(moddleContext, this[kTypeResolver], this.options.extendFn); + return moddleContextSerializer.Serializer(moddleContext, this[kTypeResolver], this.options.extendFn); }; +/** @internal */ Engine.prototype._getModdleContext = function getModdleContext(source) { - const bpmnModdle = new BpmnModdle(this.options.moddleOptions); - return bpmnModdle.fromXML(Buffer.isBuffer(source) ? source.toString() : source.trim()); + const moddle = new BpmnModdle(this.options.moddleOptions); + return moddle.fromXML(Buffer.isBuffer(source) ? source.toString() : source.trim()); }; +/** + * @param {Engine} engine + * @param {import('bpmn-elements').Definition[]} definitions + * @param {import('bpmn-engine').BpmnEngineExecuteOptions} [options] + * @param {boolean} [isRecovered] + */ function Execution(engine, definitions, options, isRecovered = false) { this.name = engine.name; this.options = options; @@ -478,6 +534,7 @@ Object.defineProperties(Execution.prototype, { }, }); +/** @internal */ Execution.prototype._execute = function execute(executeOptions, callback) { this._setup(executeOptions); this[kStopped] = false; @@ -499,6 +556,7 @@ Execution.prototype._execute = function execute(executeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._resume = function resume(resumeOptions, callback) { this._setup(resumeOptions); @@ -512,6 +570,7 @@ Execution.prototype._resume = function resume(resumeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._addConsumerCallbacks = function addConsumerCallbacks(callback) { if (!callback) return; @@ -562,6 +621,7 @@ Execution.prototype._addConsumerCallbacks = function addConsumerCallbacks(callba } }; +/** @returns {Promise} */ Execution.prototype.stop = async function stop() { const engine = this[kEngine]; const prom = engine.waitFor('stop'); @@ -579,6 +639,7 @@ Execution.prototype.stop = async function stop() { return result; }; +/** @internal */ Execution.prototype._setup = function setup(setupOptions) { const listener = setupOptions?.listener || this.options.listener; if (listener && typeof listener.emit !== 'function') throw new Error('listener.emit is not a function'); @@ -598,6 +659,7 @@ Execution.prototype._setup = function setup(setupOptions) { } }; +/** @internal */ Execution.prototype._onChildMessage = function onChildMessage(routingKey, message, owner) { const { environment: ownerEnvironment } = owner; const listener = ownerEnvironment.options?.listener; @@ -667,6 +729,7 @@ Execution.prototype._onChildMessage = function onChildMessage(routingKey, messag } }; +/** @internal */ Execution.prototype._complete = function complete(eventType, content, messageProperties) { const timers = this.environment.timers; timers.executing.slice().forEach((ref) => timers.clearTimeout(ref)); @@ -674,11 +737,13 @@ Execution.prototype._complete = function complete(eventType, content, messagePro return eventType !== 'error' && this[kEngine].emit(eventType, this); }; +/** @internal */ Execution.prototype._teardownDefinition = function teardownDefinition(definition) { this[kExecuting].delete(definition); definition.broker.cancel('_engine_definition'); }; +/** @internal */ Execution.prototype._saveOutput = function saveOutput(output) { if (!output || typeof output !== 'object') return; @@ -694,6 +759,7 @@ Execution.prototype._saveOutput = function saveOutput(output) { } }; +/** @returns {import('bpmn-engine').BpmnEngineExecutionState} */ Execution.prototype.getState = function getState() { const definitions = []; for (const definition of this.definitions) { @@ -713,6 +779,10 @@ Execution.prototype.getState = function getState() { }; }; +/** + * @param {string} activityId + * @returns {any} + */ Execution.prototype.getActivityById = function getActivityById(activityId) { for (const definition of this.definitions) { const activity = definition.getActivityById(activityId); @@ -720,6 +790,7 @@ Execution.prototype.getActivityById = function getActivityById(activityId) { } }; +/** @returns {any[]} */ Execution.prototype.getPostponed = function getPostponed() { const definitions = this.stopped ? this.definitions : this[kExecuting]; let result = []; @@ -729,6 +800,10 @@ Execution.prototype.getPostponed = function getPostponed() { return result; }; +/** + * @param {import('bpmn-engine').BpmnMessage} [payload] + * @param {{ ignoreSameDefinition?: boolean }} [signalOptions] + */ Execution.prototype.signal = function signal(payload, signalOptions) { for (const definition of this[kExecuting]) { if (signalOptions?.ignoreSameDefinition && payload?.parent?.id === definition.id) continue; @@ -736,26 +811,35 @@ Execution.prototype.signal = function signal(payload, signalOptions) { } }; +/** @param {import('bpmn-engine').BpmnMessage} [payload] */ Execution.prototype.cancelActivity = function cancelActivity(payload) { for (const definition of this[kExecuting]) { definition.cancelActivity(payload); } }; +/** + * @template T + * @param {import('bpmn-engine').BpmnEngineEvent | string} eventName + * @returns {Promise} + */ Execution.prototype.waitFor = function waitFor(...args) { return this[kEngine].waitFor(...args); }; +/** @internal */ Execution.prototype._onBrokerReturn = function onBrokerReturn(message) { if (message.properties.type === 'error') { this[kEngine].emit('error', message.content); } }; +/** @internal */ Execution.prototype._debug = function debug(msg) { this[kEngine].logger.debug(`<${this.name}> ${msg}`); }; +/** @internal */ Execution.prototype._getActivityStatus = function getActivityStatus() { let status = 'idle'; const executing = this[kExecuting]; diff --git a/package.json b/package.json index dcb229f..d6fa3cf 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { "name": "bpmn-engine", "description": "BPMN 2.0 execution engine. Open source javascript workflow engine.", - "version": "25.0.1", + "version": "26.0.6", "type": "module", "module": "./src/index.js", "main": "./lib/index.cjs", - "types": "./types/bpmn-engine.d.ts", + "types": "./types/index.d.ts", "sideEffects": false, "exports": { ".": { - "types": "./types/bpmn-engine.d.ts", + "types": "./types/index.d.ts", "require": "./lib/index.cjs", "import": "./src/index.js" } @@ -32,13 +32,12 @@ ], "scripts": { "test": "mocha -p -R @bonniernews/hot-bev", - "posttest": "npm run lint && npm run toc && npm run dist", + "posttest": "npm run lint && npm run toc && npm run dist && npm run test:md", "lint": "eslint . --cache && prettier . -c --cache", - "wintest": "mocha", "cov:html": "c8 -r html -r text mocha -p -R @bonniernews/hot-bev", "test:lcov": "c8 -r lcov mocha && npm run lint", - "toc": "node scripts/generate-api-toc ./docs/API.md,./docs/Examples.md", - "test-md": "texample ./docs/API.md,./docs/Examples.md,./docs/Upgrade.md", + "toc": "toc README.md docs/API.md docs/Examples.md docs/Upgrade.md", + "test:md": "texample ./docs/API.md,./docs/Examples.md,./docs/Upgrade.md -c .mocharc.json", "dist": "rollup -c", "prepack": "npm run dist" }, @@ -57,31 +56,45 @@ "url": "https://github.com/paed01/bpmn-engine/master/LICENSE" } ], + "overrides": { + "mocha": { + "yargs": "^18.0.0" + } + }, "devDependencies": { + "@0dep/toc": "^1.0.1", "@bonniernews/hot-bev": "^0.4.0", + "@eslint/js": "^10.0.1", "@rollup/plugin-commonjs": "^29.0.0", - "@types/bpmn-moddle": "^5.1.6", - "@types/node": "^18.19.31", + "@types/bpmn-moddle": "^10.0.0", + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", + "@types/node": "^20.19.40", "bent": "^7.3.12", - "c8": "^10.0.0", - "camunda-bpmn-moddle": "^7.0.1", + "bpmn-elements": "^18.0.24", + "bpmn-moddle": "^9.0.4", + "bpmn-moddle-10": "npm:bpmn-moddle@^10.0.0", + "c8": "^12.0.0", + "camunda-bpmn-moddle": "^8.0.1", "chai": "^6.2.0", - "chronokinesis": "^7.0.1", - "eslint": "^9.10.0", - "markdown-toc": "^1.2.0", + "chronokinesis": "^8.0.0", + "debug": "^4.4.3", + "eslint": "^10.5.0", + "globals": "^17.6.0", "mocha": "^11.0.1", "mocha-cakes-2": "^3.3.0", + "moddle-context-serializer": "^6.0.0", + "nock": "^14.0.17", "prettier": "^3.2.5", "rollup": "^4.10.0", - "texample": "^0.1.0" - }, - "dependencies": { - "bpmn-elements": "^17.2.2", - "bpmn-moddle": "^9.0.4", - "debug": "^4.4.3", - "moddle-context-serializer": "^4.4.1" + "texample": "^1.0.2", + "typescript": "^7.0.2" }, "peerDependencies": { - "smqp": ">=9" + "bpmn-elements": ">=18.0.25", + "bpmn-moddle": ">=9", + "debug": ">=4", + "moddle-context-serializer": ">=6", + "smqp": ">=15" } } diff --git a/scripts/generate-api-toc.js b/scripts/generate-api-toc.js deleted file mode 100644 index 108cb3b..0000000 --- a/scripts/generate-api-toc.js +++ /dev/null @@ -1,36 +0,0 @@ -import fs from 'node:fs'; -import { createRequire } from 'node:module'; -import { fileURLToPath } from 'node:url'; -import Toc from 'markdown-toc'; - -const nodeRequire = createRequire(fileURLToPath(import.meta.url)); -const { version } = nodeRequire('../package.json'); - -const filenames = getFileNames(); - -function getFileNames() { - const arg = process.argv[2] || './API.md'; - return arg.split(','); -} - -function generate(filename) { - const api = fs.readFileSync(filename, 'utf8'); - const tocOptions = { - bullets: '-', - slugify(text) { - return text - .toLowerCase() - .replace(/\s/g, '-') - .replace(/[^\w-]/g, ''); - }, - }; - - const output = Toc.insert(api, tocOptions).replace( - /(.|\n)*/, - '\n\n# ' + version + ' API Reference\n\n' - ); - - fs.writeFileSync(filename, output); -} - -filenames.forEach(generate); diff --git a/src/index.js b/src/index.js index 82b69dc..c1a2a76 100644 --- a/src/index.js +++ b/src/index.js @@ -2,10 +2,10 @@ import { createRequire } from 'node:module'; import { EventEmitter } from 'node:events'; import { fileURLToPath } from 'node:url'; -import BpmnModdle from 'bpmn-moddle'; +import * as bpmnModdle from 'bpmn-moddle'; import * as Elements from 'bpmn-elements'; import { Broker } from 'smqp'; -import serializer, { deserialize, TypeResolver } from 'moddle-context-serializer'; +import { Serializer, deserialize, TypeResolver } from 'moddle-context-serializer'; import DebugLogger from './Logger.js'; import JavaScripts from './JavaScripts.js'; @@ -15,6 +15,9 @@ import ProcessOutputDataObject from './extensions/ProcessOutputDataObject.js'; const nodeRequire = createRequire(fileURLToPath(import.meta.url)); const { version: engineVersion } = nodeRequire('../package.json'); +// bpmn-moddle@9 exposes the parser as default export, bpmn-moddle@10 as the named export BpmnModdle +const BpmnModdle = bpmnModdle.BpmnModdle ?? bpmnModdle.default; + const kEngine = Symbol.for('engine'); const kEnvironment = Symbol.for('environment'); const kExecuting = Symbol.for('executing'); @@ -29,6 +32,10 @@ const kTypeResolver = Symbol.for('type resolver'); export default Engine; export { JavaScripts }; +/** + * BPMN 2.0 execution engine. + * @param {import('bpmn-engine').BpmnEngineOptions} [options] + */ export function Engine(options) { if (!(this instanceof Engine)) return new Engine(options); @@ -114,6 +121,12 @@ Object.defineProperties(Engine.prototype, { }, }); +/** + * Execute the loaded definitions. + * @param {import('bpmn-engine').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {(err: Error, execution?: Execution) => void} [callback] + * @returns {Promise} + */ Engine.prototype.execute = async function execute(...args) { const [executeOptions, callback] = getOptionsAndCallback(...args); try { @@ -127,12 +140,19 @@ Engine.prototype.execute = async function execute(...args) { return execution._execute(executeOptions, callback); }; +/** @returns {Promise} */ Engine.prototype.stop = function stop() { const execution = this.execution; if (!execution) return; return execution.stop(); }; +/** + * Recover engine from saved state. + * @param {import('bpmn-engine').BpmnEngineExecutionState | null} savedState + * @param {import('bpmn-engine').BpmnEngineOptions} [recoverOptions] + * @returns {Engine} + */ Engine.prototype.recover = function recover(savedState, recoverOptions) { if (this[kExecution]?.isRunning) { throw new Error('cannot recover a running engine'); @@ -178,6 +198,12 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { return this; }; +/** + * Resume execution from a previously recovered state. + * @param {import('bpmn-engine').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {(err: Error, execution?: Execution) => void} [callback] + * @returns {Promise} + */ Engine.prototype.resume = async function resume(...args) { const [resumeOptions, callback] = getOptionsAndCallback(...args); @@ -199,6 +225,10 @@ Engine.prototype.resume = async function resume(...args) { return execution._resume(resumeOptions, callback); }; +/** + * Add a pre-serialized source context to the engine. + * @param {{ sourceContext: import('moddle-context-serializer').SerializableContext }} [options] + */ Engine.prototype.addSource = function addSource(options) { if (!options?.sourceContext) return; const loadedDefinitions = this[kLoadedDefinitions]; @@ -206,16 +236,25 @@ Engine.prototype.addSource = function addSource(options) { this[kPendingSources].add(options.sourceContext); }; +/** + * @param {import('bpmn-engine').BpmnEngineExecuteOptions} [executeOptions] + * @returns {Promise} + */ Engine.prototype.getDefinitions = function getDefinitions(executeOptions) { const loadedDefinitions = this[kLoadedDefinitions]; if (loadedDefinitions?.length) return Promise.resolve(loadedDefinitions); return this._loadDefinitions(executeOptions); }; +/** + * @param {string} id + * @returns {Promise} + */ Engine.prototype.getDefinitionById = async function getDefinitionById(id) { return (await this.getDefinitions()).find((d) => d.id === id); }; +/** @returns {Promise} */ Engine.prototype.getState = async function getState() { const execution = this.execution; if (execution) return execution.getState(); @@ -224,6 +263,11 @@ Engine.prototype.getState = async function getState() { return new Execution(this, definitions, this.options).getState(); }; +/** + * @template R + * @param {import('bpmn-engine').BpmnEngineEvent | string} eventName + * @returns {Promise} + */ Engine.prototype.waitFor = function waitFor(eventName) { const self = this; return new Promise((resolve, reject) => { @@ -241,12 +285,14 @@ Engine.prototype.waitFor = function waitFor(eventName) { }); }; +/** @internal */ Engine.prototype._loadDefinitions = async function loadDefinitions(executeOptions) { const runSources = await Promise.all(this[kPendingSources]); const loadedDefinitions = (this[kLoadedDefinitions] = runSources.map((source) => this._loadDefinition(source, executeOptions))); return loadedDefinitions; }; +/** @internal */ Engine.prototype._loadDefinition = function loadDefinition(serializedContext, executeOptions) { const environment = this.environment; const context = new Elements.Context( @@ -269,20 +315,29 @@ Engine.prototype._loadDefinition = function loadDefinition(serializedContext, ex return new Elements.Definition(context); }; +/** @internal */ Engine.prototype._serializeSource = async function serializeSource(source) { const moddleContext = await this._getModdleContext(source); return this._serializeModdleContext(moddleContext); }; +/** @internal */ Engine.prototype._serializeModdleContext = function serializeModdleContext(moddleContext) { - return serializer(moddleContext, this[kTypeResolver], this.options.extendFn); + return Serializer(moddleContext, this[kTypeResolver], this.options.extendFn); }; +/** @internal */ Engine.prototype._getModdleContext = function getModdleContext(source) { - const bpmnModdle = new BpmnModdle(this.options.moddleOptions); - return bpmnModdle.fromXML(Buffer.isBuffer(source) ? source.toString() : source.trim()); + const moddle = new BpmnModdle(this.options.moddleOptions); + return moddle.fromXML(Buffer.isBuffer(source) ? source.toString() : source.trim()); }; +/** + * @param {Engine} engine + * @param {import('bpmn-elements').Definition[]} definitions + * @param {import('bpmn-engine').BpmnEngineExecuteOptions} [options] + * @param {boolean} [isRecovered] + */ export function Execution(engine, definitions, options, isRecovered = false) { this.name = engine.name; this.options = options; @@ -332,6 +387,7 @@ Object.defineProperties(Execution.prototype, { }, }); +/** @internal */ Execution.prototype._execute = function execute(executeOptions, callback) { this._setup(executeOptions); this[kStopped] = false; @@ -353,6 +409,7 @@ Execution.prototype._execute = function execute(executeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._resume = function resume(resumeOptions, callback) { this._setup(resumeOptions); @@ -366,6 +423,7 @@ Execution.prototype._resume = function resume(resumeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._addConsumerCallbacks = function addConsumerCallbacks(callback) { if (!callback) return; @@ -416,6 +474,7 @@ Execution.prototype._addConsumerCallbacks = function addConsumerCallbacks(callba } }; +/** @returns {Promise} */ Execution.prototype.stop = async function stop() { const engine = this[kEngine]; const prom = engine.waitFor('stop'); @@ -433,6 +492,7 @@ Execution.prototype.stop = async function stop() { return result; }; +/** @internal */ Execution.prototype._setup = function setup(setupOptions) { const listener = setupOptions?.listener || this.options.listener; if (listener && typeof listener.emit !== 'function') throw new Error('listener.emit is not a function'); @@ -452,6 +512,7 @@ Execution.prototype._setup = function setup(setupOptions) { } }; +/** @internal */ Execution.prototype._onChildMessage = function onChildMessage(routingKey, message, owner) { const { environment: ownerEnvironment } = owner; const listener = ownerEnvironment.options?.listener; @@ -521,6 +582,7 @@ Execution.prototype._onChildMessage = function onChildMessage(routingKey, messag } }; +/** @internal */ Execution.prototype._complete = function complete(eventType, content, messageProperties) { const timers = this.environment.timers; timers.executing.slice().forEach((ref) => timers.clearTimeout(ref)); @@ -528,11 +590,13 @@ Execution.prototype._complete = function complete(eventType, content, messagePro return eventType !== 'error' && this[kEngine].emit(eventType, this); }; +/** @internal */ Execution.prototype._teardownDefinition = function teardownDefinition(definition) { this[kExecuting].delete(definition); definition.broker.cancel('_engine_definition'); }; +/** @internal */ Execution.prototype._saveOutput = function saveOutput(output) { if (!output || typeof output !== 'object') return; @@ -548,6 +612,7 @@ Execution.prototype._saveOutput = function saveOutput(output) { } }; +/** @returns {import('bpmn-engine').BpmnEngineExecutionState} */ Execution.prototype.getState = function getState() { const definitions = []; for (const definition of this.definitions) { @@ -567,6 +632,10 @@ Execution.prototype.getState = function getState() { }; }; +/** + * @param {string} activityId + * @returns {any} + */ Execution.prototype.getActivityById = function getActivityById(activityId) { for (const definition of this.definitions) { const activity = definition.getActivityById(activityId); @@ -574,6 +643,7 @@ Execution.prototype.getActivityById = function getActivityById(activityId) { } }; +/** @returns {any[]} */ Execution.prototype.getPostponed = function getPostponed() { const definitions = this.stopped ? this.definitions : this[kExecuting]; let result = []; @@ -583,6 +653,10 @@ Execution.prototype.getPostponed = function getPostponed() { return result; }; +/** + * @param {import('bpmn-engine').BpmnMessage} [payload] + * @param {{ ignoreSameDefinition?: boolean }} [signalOptions] + */ Execution.prototype.signal = function signal(payload, signalOptions) { for (const definition of this[kExecuting]) { if (signalOptions?.ignoreSameDefinition && payload?.parent?.id === definition.id) continue; @@ -590,26 +664,35 @@ Execution.prototype.signal = function signal(payload, signalOptions) { } }; +/** @param {import('bpmn-engine').BpmnMessage} [payload] */ Execution.prototype.cancelActivity = function cancelActivity(payload) { for (const definition of this[kExecuting]) { definition.cancelActivity(payload); } }; +/** + * @template T + * @param {import('bpmn-engine').BpmnEngineEvent | string} eventName + * @returns {Promise} + */ Execution.prototype.waitFor = function waitFor(...args) { return this[kEngine].waitFor(...args); }; +/** @internal */ Execution.prototype._onBrokerReturn = function onBrokerReturn(message) { if (message.properties.type === 'error') { this[kEngine].emit('error', message.content); } }; +/** @internal */ Execution.prototype._debug = function debug(msg) { this[kEngine].logger.debug(`<${this.name}> ${msg}`); }; +/** @internal */ Execution.prototype._getActivityStatus = function getActivityStatus() { let status = 'idle'; const executing = this[kExecuting]; diff --git a/test/Engine-test.js b/test/Engine-test.js index bd2e421..2e431e5 100644 --- a/test/Engine-test.js +++ b/test/Engine-test.js @@ -1,5 +1,6 @@ import { EventEmitter } from 'node:events'; import * as Bpmn from 'bpmn-engine'; +// eslint-disable-next-line no-duplicate-imports import Engine from 'bpmn-engine'; import * as factory from './helpers/factory.js'; import * as testHelpers from './helpers/testHelpers.js'; @@ -43,6 +44,7 @@ describe('Engine', () => { it('throws if unsupported source is passed', (done) => { const engine = Bpmn.Engine({ + // @ts-expect-error source: {}, }); @@ -65,6 +67,7 @@ describe('Engine', () => { it('but not function', (done) => { const engine = Bpmn.Engine({ + // @ts-expect-error source() {}, }); @@ -122,6 +125,7 @@ describe('Engine', () => { expect(() => Bpmn.Engine({ source: factory.valid(), + // @ts-expect-error Logger, }) ).to.throw(TypeError, 'Logger is not a function'); @@ -305,7 +309,7 @@ describe('Engine', () => { input: 0, }, services: { - get(context, next) { + get(_context, next) { next(new Error('Inner error')); }, }, @@ -432,7 +436,7 @@ describe('Engine', () => { name: 'end test', source, services: { - get: (context, next) => { + get: (_context, next) => { next(new Error('Inner error')); }, }, @@ -460,7 +464,7 @@ describe('Engine', () => { name: 'end test', source, services: { - get: (context, next) => { + get: (_context, next) => { next(new Error('Inner error')); }, }, @@ -481,6 +485,7 @@ describe('Engine', () => { try { await engine.execute({ + // @ts-expect-error listener: {}, }); } catch (e) { @@ -1155,6 +1160,7 @@ describe('Engine', () => { it('execution api returns activities in a postponed state', async () => { const listener = new EventEmitter(); + /** @type {import('bpmn-engine').Execution} */ let engineApi; listener.once('wait', (_, api) => { engineApi = api; @@ -1229,6 +1235,7 @@ describe('Engine', () => { sourceContext: updateContext, }); + /** @type {import('bpmn-engine').Execution} */ let engineApi; listener.once('wait', (_, api) => { engineApi = api; diff --git a/test/feature/Engine-feature.js b/test/feature/Engine-feature.js index 78a63ca..60dec87 100644 --- a/test/feature/Engine-feature.js +++ b/test/feature/Engine-feature.js @@ -5,7 +5,10 @@ import { camundaBpmnModdle as camunda } from '../helpers/testHelpers.js'; Feature('Engine', () => { Scenario('Mother of all', () => { - let engine, source; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {Buffer} */ + let source; Given('a massive source with user task, sub process, lanes, and a loop back', () => { source = factory.resource('mother-of-all.bpmn'); }); @@ -126,7 +129,10 @@ Feature('Engine', () => { }); Scenario('A definition with lanes', () => { - let engine, source; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {Buffer} */ + let source; Given('a bpmn source with two lanes with message flows', () => { source = factory.resource('lanes.bpmn'); }); @@ -167,7 +173,10 @@ Feature('Engine', () => { }); Scenario('Activity extension', () => { - let engine, source; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {string} */ + let source; Given('a bpmn source with user tasks', () => { source = ` { }); Scenario('Execute with callback', () => { - let engine, source; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {Buffer} */ + let source; Given('a massive source with user task, timeouts, and the rest', () => { source = factory.resource('mother-of-all.bpmn'); }); @@ -316,7 +328,10 @@ Feature('Engine', () => { }); Scenario('Resume with callback', () => { - let engine, source; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {Buffer} */ + let source; Given('a massive source with user task, timeouts, and the rest', () => { source = factory.resource('mother-of-all.bpmn'); }); @@ -336,7 +351,7 @@ Feature('Engine', () => { let listener; And('expects to be stopped at first user task wait', () => { listener = new EventEmitter(); - listener.once('activity.wait', (activityApi, engineApi) => { + listener.once('activity.wait', (_activityApi, engineApi) => { engineApi.stop(); }); }); diff --git a/test/feature/backward-compatibility-feature.js b/test/feature/backward-compatibility-feature.js index deb5a6f..84be4f0 100644 --- a/test/feature/backward-compatibility-feature.js +++ b/test/feature/backward-compatibility-feature.js @@ -86,6 +86,7 @@ Feature('Backward compatability', () => { delete state.definitions[0].execution.processes[0].environment; }); + /** @type {Engine} */ let recovered; When('engine is recovered with old state in latest version', () => { const upgradedState = upgradeStateToVersion14(state); diff --git a/test/feature/bpmn-moddle-backward-compatibility-feature.js b/test/feature/bpmn-moddle-backward-compatibility-feature.js new file mode 100644 index 0000000..244fd14 --- /dev/null +++ b/test/feature/bpmn-moddle-backward-compatibility-feature.js @@ -0,0 +1,131 @@ +import { execFile } from 'node:child_process'; +import { EventEmitter } from 'node:events'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; + +import { Engine } from 'bpmn-engine'; +// @ts-ignore +import BpmnModdle9 from 'bpmn-moddle'; +import { BpmnModdle as BpmnModdle10 } from 'bpmn-moddle-10'; + +import * as factory from '../helpers/factory.js'; + +/** + * bpmn-moddle is a peer dependency with the range `>=9`, so a consumer may run + * the engine on either v9 or v10. v10 ships the parser as a *named* export + * (`import { BpmnModdle }`) instead of the v9 default export, but the moddle + * context it produces is identical. These features pin that compatibility down: + * state persisted by a deployment on v9 must keep resuming after an upgrade to v10. + */ +Feature('bpmn-moddle 9/10 backward compatibility', () => { + const source = factory.resource('mother-of-all.bpmn').toString(); + const services = { + serviceFn(...args) { + args.pop()(); + }, + }; + + Scenario('state saved on bpmn-moddle@9 is resumed on bpmn-moddle@10', () => { + let state; + Given('the mother-of-all source parsed with bpmn-moddle@9', () => { + expect(BpmnModdle9, 'v9 default export').to.be.a('function'); + }); + + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {Promise} */ + let stopped; + And('an engine started from that v9 moddle context', async () => { + const moddleContext = await new BpmnModdle9().fromXML(source.trim()); + engine = new Engine({ name: 'saved on v9', moddleContext, services }); + + const listener = new EventEmitter(); + listener.once('activity.wait', (_, engineApi) => engineApi.stop()); + + stopped = engine.waitFor('stop'); + engine.execute({ listener }); + }); + + And('it is stopped at the first user task wait', () => { + return stopped; + }); + + Then('engine state can be saved', async () => { + state = await engine.getState(); + expect(state.definitions[0]).to.have.property('id', 'Definitions_1'); + expect(state.definitions[0]).to.have.property('source').that.is.a('string'); + }); + + let slimmerState; + And('the persisted runtime state without the embedded v9 source', () => { + slimmerState = JSON.parse(JSON.stringify(state)); + slimmerState.definitions[0].source = undefined; + }); + + /** @type {import('bpmn-engine').Engine} */ + let recovered; + /** @type {Promise} */ + let ended; + When('a fresh engine parses the same source with bpmn-moddle@10 and recovers the state', async () => { + expect(BpmnModdle10, 'v10 named export').to.be.a('function'); + + const moddleContext = await new BpmnModdle10().fromXML(source.trim()); + recovered = new Engine({ name: 'resumed on v10', moddleContext, services }); + recovered.recover(slimmerState); + }); + + And('the recovered engine is resumed, signalling every wait', () => { + const listener = new EventEmitter(); + listener.on('activity.wait', (activityApi) => activityApi.signal()); + + ended = recovered.waitFor('end'); + recovered.resume({ listener }); + }); + + Then('execution completes', async () => { + await ended; + expect(recovered).to.have.property('state', 'idle'); + }); + }); + + Scenario('bpmn-moddle@9 and bpmn-moddle@10 serialize to the same source context', () => { + let v9Context, v10Context; + Given('the source serialized through an engine using bpmn-moddle@9', async () => { + const moddleContext = await new BpmnModdle9().fromXML(source.trim()); + v9Context = await new Engine({ moddleContext }).getDefinitions().then((defs) => defs[0].environment.options.source); + }); + + And('the source serialized through an engine using bpmn-moddle@10', async () => { + const moddleContext = await new BpmnModdle10().fromXML(source.trim()); + v10Context = await new Engine({ moddleContext }).getDefinitions().then((defs) => defs[0].environment.options.source); + }); + + Then('both serialized contexts are equal', () => { + expect(JSON.parse(v9Context.serialize())).to.deep.equal(JSON.parse(v10Context.serialize())); + }); + }); + + Scenario('the engine parses raw source itself regardless of bpmn-moddle version', () => { + Given('the engine parses the source option with bpmn-moddle@9, the installed peer', async () => { + const definitions = await new Engine({ source }).getDefinitions(); + expect(definitions[0]).to.have.property('id', 'Definitions_1'); + }); + + let v10Run; + When('a child process where bpmn-moddle resolves to v10 runs the engine with the source option', async function whenSpawned() { + this.timeout(5000); + const helperDir = new URL('../helpers/bpmn-moddle-10/', import.meta.url); + const { stdout } = await promisify(execFile)(process.execPath, [ + '--import', + new URL('./register.js', helperDir).href, + fileURLToPath(new URL('./run-engine-source.js', helperDir)), + ]); + v10Run = JSON.parse(stdout); + }); + + Then('the child process resolved the v10 named export and the engine parsed the source', () => { + expect(v10Run.resolvedModdleExports, 'v10 exports').to.deep.equal(['BpmnModdle']); + expect(v10Run).to.have.property('definitionId', 'Definitions_1'); + }); + }); +}); diff --git a/test/feature/call-activity-feature.js b/test/feature/call-activity-feature.js index 5f89c49..e26a7ce 100644 --- a/test/feature/call-activity-feature.js +++ b/test/feature/call-activity-feature.js @@ -108,6 +108,7 @@ Feature('Call activity', () => { }); Scenario('call activity is canceled', () => { + /** @type {Engine} */ let engine; Given('a process with a call activity referencing a process', () => { const source = ` @@ -201,6 +202,7 @@ Feature('Call activity', () => { }); Scenario('call activity is discarded mid run', () => { + /** @type {Engine} */ let engine; Given('a process with a call activity referencing a process', () => { const source = ` @@ -244,6 +246,7 @@ Feature('Call activity', () => { }); Scenario('a process with a parallel multi-instance call activity with cardinality of three', () => { + /** @type {Engine} */ let engine; const serviceCalls = []; Given('two processes', () => { diff --git a/test/feature/conditional-event-feature.js b/test/feature/conditional-event-feature.js index 8d99d2c..fd83e6e 100644 --- a/test/feature/conditional-event-feature.js +++ b/test/feature/conditional-event-feature.js @@ -5,7 +5,9 @@ const boundJsEventSource = factory.resource('conditional-bound-js-event.bpmn'); Feature('Conditional event', () => { Scenario('A service with conditional bound expression event', () => { - let source, engine, serviceCallback; + /** @type {Engine} */ + let engine; + let source, serviceCallback; Given('a source matching scenario', () => { source = ` diff --git a/test/feature/engine-output-feature.js b/test/feature/engine-output-feature.js index 4a0605c..a2b32cb 100644 --- a/test/feature/engine-output-feature.js +++ b/test/feature/engine-output-feature.js @@ -2,6 +2,7 @@ import { Engine } from 'bpmn-engine'; Feature('Engine output', () => { Scenario('Process completes with output', () => { + /** @type {Engine} */ let engine; Given('a process with output', () => { const source = ` @@ -77,6 +78,7 @@ Feature('Engine output', () => { }); Scenario('process fails', () => { + /** @type {Engine} */ let engine; Given('a process with output', () => { const source = ` diff --git a/test/feature/extend-feature.js b/test/feature/extend-feature.js index 0ef1c86..722cca0 100644 --- a/test/feature/extend-feature.js +++ b/test/feature/extend-feature.js @@ -149,7 +149,7 @@ Feature('extending behaviour', () => { camunda, }, services: { - serviceFn(scope, callback) { + serviceFn(_scope, callback) { callback(null, { data: 1 }); }, }, @@ -223,7 +223,7 @@ Feature('extending behaviour', () => { source, scripts: { register() {}, - getScript(scriptType, activity) { + getScript(_scriptType, activity) { if (activity.id === 'task1') { return { execute(scope, next) { @@ -509,7 +509,7 @@ Feature('extending behaviour', () => { And('second flow was discarded', () => { const flow = execution.definitions[0].getProcesses()[0].context.getSequenceFlowById('flow3'); - expect(flow.counters).to.have.property('discard', 1); + expect(flow.counters).to.have.property('take', 0); }); Given('an engine with type resolver function with new behaviour', () => { @@ -547,9 +547,9 @@ Feature('extending behaviour', () => { expect(flow.counters).to.have.property('take', 1); }); - And('second flow was discarded', () => { + And('second flow was not taken', () => { const flow = execution.definitions[0].getProcesses()[0].context.getSequenceFlowById('flow3'); - expect(flow.counters).to.have.property('discard', 1); + expect(flow.counters).to.have.property('take', 0); }); }); @@ -598,7 +598,7 @@ Feature('extending behaviour', () => { for (const extension of activity.behaviour.extensionElements.values) { switch (extension.$type) { case 'camunda:ExecutionListener': { - myExtensions.push(ExecutionListener(extension)); + if (extension.script) myExtensions.push(ExecutionListener(extension)); break; } } @@ -621,7 +621,7 @@ Feature('extending behaviour', () => { broker.subscribeTmp( 'event', `activity.${extension.event}`, - (routingKey, message) => { + (_routingKey, message) => { script.execute(message); }, { noAck: true, consumerTag: '_my-extension' } diff --git a/test/feature/issues-feature.js b/test/feature/issues-feature.js index 2c27a14..31ac779 100644 --- a/test/feature/issues-feature.js +++ b/test/feature/issues-feature.js @@ -118,7 +118,7 @@ Feature('Issues', () => { name: 'issue 105', source: source1, services: { - async doTask1(scope, callback) { + async doTask1(_scope, callback) { await sleep(50); // calling other heavy service... return callback(null); }, @@ -176,16 +176,14 @@ Feature('Issues', () => { return end; }); - And('user task was discarded once', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 1); }); - And('end was discarded thrice', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 3); }); When('executed again', async () => { @@ -229,16 +227,14 @@ Feature('Issues', () => { return end; }); - And('user task was discarded once', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 1); }); - And('end was discarded thrice', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 3); }); When('execution is recovered with state from first run user task wait', () => { @@ -279,16 +275,14 @@ Feature('Issues', () => { return end; }); - And('user task was discarded once', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 1); }); - And('end was discarded thrice', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 3); }); Given('ran again', async () => { @@ -336,8 +330,8 @@ Feature('Issues', () => { execution = await engine.resume(); }); - Then('end event is discarded once', () => { - expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 0, discarded: 1 }); + Then('end event is not taken', () => { + expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); When('user task is signaled', () => { @@ -348,8 +342,8 @@ Feature('Issues', () => { return end; }); - Then('end event is taken once and discarded thrice', () => { - expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 1, discarded: 3 }); + Then('end event is taken once', () => { + expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 1, discarded: 0 }); }); }); @@ -361,7 +355,7 @@ Feature('Issues', () => { name: 'issue 105', source: source2, services: { - async doTask1(scope, callback) { + async doTask1(_scope, callback) { await sleep(50); // calling other heavy service... return callback(null); }, @@ -419,16 +413,16 @@ Feature('Issues', () => { return end; }); - And('user task was discarded twice', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 2); + expect(task.counters).to.have.property('discarded', 0); }); - And('end was discarded four times', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 4); + expect(task.counters).to.have.property('discarded', 0); }); When('executed again', async () => { @@ -472,16 +466,16 @@ Feature('Issues', () => { return end; }); - And('user task was discarded twice', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 2); + expect(task.counters).to.have.property('discarded', 0); }); - And('end was discarded four times', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 4); + expect(task.counters).to.have.property('discarded', 0); }); When('execution is recovered with state from first run user task wait', () => { @@ -522,16 +516,16 @@ Feature('Issues', () => { return end; }); - And('user task was discarded twice', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 2); + expect(task.counters).to.have.property('discarded', 0); }); - And('end was discarded four times', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 4); + expect(task.counters).to.have.property('discarded', 0); }); Given('ran again', async () => { @@ -570,7 +564,7 @@ Feature('Issues', () => { end = engine.waitFor('end'); }); - Then('end event is still not discarded', async () => { + Then('end event is still not touched', async () => { const [definition] = await engine.getDefinitions(); expect(definition.getActivityById('End').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); @@ -579,8 +573,8 @@ Feature('Issues', () => { execution = await engine.resume(); }); - Then('end event is discarded once', () => { - expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 0, discarded: 1 }); + Then('end event is not taken', () => { + expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); When('user task is signaled', () => { @@ -591,16 +585,16 @@ Feature('Issues', () => { return end; }); - And('user task was taken once and discarded twice', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 2); + expect(task.counters).to.have.property('discarded', 0); }); - And('end was taken once and discarded four times', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 4); + expect(task.counters).to.have.property('discarded', 0); }); }); }); @@ -718,8 +712,8 @@ Feature('Issues', () => { expect(execution.getActivityById('task3').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); - And('fourth user task was discarded', () => { - expect(execution.getActivityById('task4').counters).to.deep.equal({ taken: 0, discarded: 1 }); + And('fourth user task was not taken', () => { + expect(execution.getActivityById('task4').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); let end; @@ -753,8 +747,8 @@ Feature('Issues', () => { expect(execution.getActivityById('task3').counters).to.deep.equal({ taken: 1, discarded: 0 }); }); - And('fourth user task was discarded', () => { - expect(execution.getActivityById('task4').counters).to.deep.equal({ taken: 0, discarded: 1 }); + And('fourth user task was not taken', () => { + expect(execution.getActivityById('task4').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); And('execution completed', () => { @@ -1111,11 +1105,11 @@ Feature('Issues', () => { let output; if (content.isMultiInstance) { - output = rawOutput.map(({ id, executionId /* eslint-disable-line no-unused-vars */, ...rest }) => { + output = rawOutput.map(({ id, executionId, ...rest }) => { return rest; }); } else { - const { id, executionId, ...rest } = rawOutput; // eslint-disable-line no-unused-vars + const { id, executionId, ...rest } = rawOutput; output = rest; } @@ -1327,15 +1321,22 @@ function TimersWithoutScope(options) { } function registerTimeout(owner) { - return function registeredSetTimeout(...args) { - return timersApi.setTimeout.call(owner, ...args); + return function registeredSetTimeout(callback, delay, ...args) { + return timersApi.setTimeout.call(owner, callback, delay, ...args); }; } + /** @this {any} */ function wrappedSetTimeout(callback, delay, ...args) { - const ref = { timerId: `timer_${count++}`, callback, delay, args, owner: this }; + const ref = { + timerId: `timer_${count++}`, + callback, + delay, + args, + owner: this, + timerRef: options.setTimeout.call(null, onTimeout, delay, ...args), + }; executing.push(ref); - ref.timerRef = options.setTimeout.call(null, onTimeout, delay, ...args); return ref; function onTimeout(...rargs) { diff --git a/test/feature/issues/issue-163-feature.js b/test/feature/issues/issue-163-feature.js index d388496..2450dc8 100644 --- a/test/feature/issues/issue-163-feature.js +++ b/test/feature/issues/issue-163-feature.js @@ -3,7 +3,10 @@ import * as factory from '../../helpers/factory.js'; Feature('issue 163', () => { Scenario('Get sub process postponed', () => { - let engine, execution; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {import('bpmn-engine').Execution} */ + let execution; When('one usertask and two user tasks in sub process and finally a user task', async () => { const source = factory.resource('issue-163.bpmn'); diff --git a/test/feature/issues/issue-172-feature.js b/test/feature/issues/issue-172-feature.js index f924ab4..9531f1e 100644 --- a/test/feature/issues/issue-172-feature.js +++ b/test/feature/issues/issue-172-feature.js @@ -2,6 +2,7 @@ import { Engine } from 'bpmn-engine'; Feature('issue 172', () => { Scenario('Postpone intermediate throw event by formatting', () => { + /** @type {import('bpmn-engine').Engine} */ let engine; const agiCalls = []; Given('start event, intermediate throw event, and end event', () => { diff --git a/test/feature/issues/issue-187-feature.js b/test/feature/issues/issue-187-feature.js index 269135e..1cedd56 100644 --- a/test/feature/issues/issue-187-feature.js +++ b/test/feature/issues/issue-187-feature.js @@ -6,6 +6,7 @@ const source = factory.resource('issue-187.bpmn'); Feature('issue 187 - Complete condition in multi-instance loop with custom expression parser', () => { Scenario('user task is looped over list of handlers', () => { + /** @type {import('bpmn-engine').Engine} */ let engine; Given('start event, intermediate throw event, and end event', () => { engine = getEngine187(); diff --git a/test/feature/issues/issue-199-feature.js b/test/feature/issues/issue-199-feature.js index 1689c31..fcacf60 100644 --- a/test/feature/issues/issue-199-feature.js +++ b/test/feature/issues/issue-199-feature.js @@ -36,6 +36,7 @@ next(); Feature('issue 199 - Issue with Script Tasks After State Recovery in bpmn-engine', () => { Scenario('execute, recover resume with same engine instance', () => { + /** @type {import('bpmn-engine').Engine} */ let engine; Given('an engine with user task flanked by two script tasks', () => { engine = new Engine({ diff --git a/test/feature/multiple-sources-feature.js b/test/feature/multiple-sources-feature.js index 3ea7d42..3064ce7 100644 --- a/test/feature/multiple-sources-feature.js +++ b/test/feature/multiple-sources-feature.js @@ -6,6 +6,18 @@ import { camundaBpmnModdle as camunda } from '../helpers/testHelpers.js'; const signalsSource = factory.resource('signals.bpmn'); const sendSignalSource = factory.resource('send-signal.bpmn'); +class DataStores { + constructor(data) { + this.data = data; + } + getDataStore(id) { + return this.data[id]; + } + setDataStore(id, value) { + this.data[id] = value; + } +} + Feature('Multiple sources', () => { Scenario('Two definitions that communicates with signals', () => { let engine, signalContext, updateContext; @@ -687,7 +699,7 @@ function getExtendedEngine(options) { engine.broker.subscribeTmp( 'event', 'activity.signal', - (routingKey, msg) => { + (_routingKey, msg) => { engine.execution.signal(msg.content.message, { ignoreSameDefinition: true }); }, { noAck: true } @@ -713,7 +725,7 @@ function ServiceExpression(activity) { } } -function formFormatting(activity, context, formData) { +function formFormatting(activity, _context, formData) { const { broker, environment } = activity; broker.subscribeTmp( 'event', @@ -732,7 +744,7 @@ function formFormatting(activity, context, formData) { ); } -function ioFormatting(activity, context, ioData) { +function ioFormatting(activity, _context, ioData) { const { broker, environment } = activity; if (ioData.inputParameters) { broker.subscribeTmp( @@ -766,15 +778,3 @@ function ioFormatting(activity, context, ioData) { ); } } - -function DataStores(data) { - this.data = data; -} - -DataStores.prototype.getDataStore = function getDataStore(id) { - return this.data[id]; -}; - -DataStores.prototype.setDataStore = function setDataStore(id, value) { - this.data[id] = value; -}; diff --git a/test/feature/resume-feature.js b/test/feature/resume-feature.js index ba3e052..5feafdc 100644 --- a/test/feature/resume-feature.js +++ b/test/feature/resume-feature.js @@ -4,7 +4,9 @@ import { Engine } from 'bpmn-engine'; Feature('Resume execution', () => { Scenario('Execution is stopped and resumed', () => { - let engine, listener, source; + /** @type {Engine} */ + let engine; + let listener, source; Given('a bpmn source with a sub processes with one user task', () => { source = ` @@ -280,7 +282,9 @@ Feature('Resume execution', () => { }); Scenario('recover with options', () => { - let engine, listener, source; + /** @type {Engine} */ + let engine; + let listener, source; Given('a bpmn source with user task, timer and script', () => { source = ` diff --git a/test/feature/scripts-feature.js b/test/feature/scripts-feature.js index f4478d7..85ae0a3 100644 --- a/test/feature/scripts-feature.js +++ b/test/feature/scripts-feature.js @@ -2,7 +2,9 @@ import { Engine } from 'bpmn-engine'; Feature('Scripts', () => { Scenario('Process with scripts', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a bpmn source with empty script task', () => { source = ` { }); Scenario('Process with setTimeout in inline scripts task', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a bpmn source with script task with setTimeout', () => { source = ` { }); Scenario('Process with long running timer can be stopped', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a bpmn source with script task with setTimeout', () => { source = ` { Scenario('Determine run sequences for an activity', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a bpmn source with user tasks', () => { source = ` { `; }); + /** @type {import('bpmn-elements').Definition} */ let definition; And('an engine loaded with extension for fetching form and saving output', async () => { engine = Engine({ diff --git a/test/feature/timers-feature.js b/test/feature/timers-feature.js index 1fbf1a1..b8577c9 100644 --- a/test/feature/timers-feature.js +++ b/test/feature/timers-feature.js @@ -16,6 +16,7 @@ Feature('Timers', () => { }); after(ck.reset); + /** @type {Engine} */ let engine; Given('a time cycle start event, bound time duration event, throw time date event, and a user task with due date', async () => { const sourceContext = await testHelpers.context(timersSource, { @@ -244,7 +245,9 @@ Feature('Timers', () => { }); Scenario('engine with added arbitrary timers', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a source with user task and a bound timer event', () => { source = ` @@ -357,7 +360,9 @@ Feature('Timers', () => { }); Scenario('engine with invalid timers', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a source with user task and a bound timer event with invalid date', () => { source = ` diff --git a/test/globals.d.ts b/test/globals.d.ts new file mode 100644 index 0000000..8cbd646 --- /dev/null +++ b/test/globals.d.ts @@ -0,0 +1,16 @@ +declare global { + const expect: Chai.ExpectStatic; + + // mocha-cakes-2 + const Feature: Mocha.SuiteFunction; + const Scenario: Mocha.SuiteFunction; + const Given: Mocha.TestFunction; + const When: Mocha.TestFunction; + const Then: Mocha.TestFunction; + const And: Mocha.TestFunction; + const But: Mocha.TestFunction; + const afterEachScenario: Mocha.HookFunction; + const beforeEachScenario: Mocha.HookFunction; +} + +export {}; diff --git a/test/helpers/bpmn-moddle-10/hooks.js b/test/helpers/bpmn-moddle-10/hooks.js new file mode 100644 index 0000000..7b0b2c3 --- /dev/null +++ b/test/helpers/bpmn-moddle-10/hooks.js @@ -0,0 +1,9 @@ +/** + * Module resolve hook that redirects every `bpmn-moddle` specifier to the + * `bpmn-moddle-10` npm alias (bpmn-moddle@^10), so a child process can run + * the engine as if bpmn-moddle 10 were the installed peer dependency. + */ +export function resolve(specifier, context, nextResolve) { + if (specifier === 'bpmn-moddle') return nextResolve('bpmn-moddle-10', context); + return nextResolve(specifier, context); +} diff --git a/test/helpers/bpmn-moddle-10/register.js b/test/helpers/bpmn-moddle-10/register.js new file mode 100644 index 0000000..19d345c --- /dev/null +++ b/test/helpers/bpmn-moddle-10/register.js @@ -0,0 +1,3 @@ +import { register } from 'node:module'; + +register(new URL('./hooks.js', import.meta.url)); diff --git a/test/helpers/bpmn-moddle-10/run-engine-source.js b/test/helpers/bpmn-moddle-10/run-engine-source.js new file mode 100644 index 0000000..98feb4a --- /dev/null +++ b/test/helpers/bpmn-moddle-10/run-engine-source.js @@ -0,0 +1,31 @@ +/** + * Runs the engine with the raw `source` option — the path that makes the + * engine itself import and instantiate bpmn-moddle. Launched by the + * bpmn-moddle backward compatibility feature with `--import ./register.js`, + * so `bpmn-moddle` resolves to bpmn-moddle@10 in this process. + * + * Prints a JSON verdict on stdout and exits non-zero on failure. + */ +import * as bpmnModdle from 'bpmn-moddle'; +import { Engine } from 'bpmn-engine'; + +import * as factory from '../factory.js'; + +const engine = new Engine({ + name: 'source parsed by the engine on bpmn-moddle@10', + source: factory.resource('mother-of-all.bpmn').toString(), + services: { + serviceFn(...args) { + args.pop()(); + }, + }, +}); + +const definitions = await engine.getDefinitions(); + +process.stdout.write( + JSON.stringify({ + resolvedModdleExports: Object.keys(bpmnModdle), + definitionId: definitions[0].id, + }) +); diff --git a/test/helpers/factory.js b/test/helpers/factory.js index 0b694c9..81a54ec 100644 --- a/test/helpers/factory.js +++ b/test/helpers/factory.js @@ -83,6 +83,10 @@ export function multipleInbound() { `; } +/** + * @param {string} name + * @returns {Buffer} + */ export function resource(name) { return fs.readFileSync(path.join('./test/resources', name)); } diff --git a/test/helpers/setup.js b/test/helpers/setup.js new file mode 100644 index 0000000..74e2dce --- /dev/null +++ b/test/helpers/setup.js @@ -0,0 +1,5 @@ +import nock from 'nock'; + +process.env.NODE_ENV = 'test'; + +nock.enableNetConnect(/localhost|127\.0\.0\.1/); diff --git a/test/helpers/testHelpers.js b/test/helpers/testHelpers.js index fabc45c..0a5262d 100644 --- a/test/helpers/testHelpers.js +++ b/test/helpers/testHelpers.js @@ -1,10 +1,11 @@ import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; +// @ts-ignore import BpmnModdle from 'bpmn-moddle'; import * as Elements from 'bpmn-elements'; import Logger from '../../src/Logger.js'; -import serializer, { TypeResolver } from 'moddle-context-serializer'; +import { Serializer, TypeResolver } from 'moddle-context-serializer'; const nodeRequire = createRequire(fileURLToPath(import.meta.url)); export const camundaBpmnModdle = nodeRequire('camunda-bpmn-moddle/resources/camunda.json'); @@ -25,7 +26,7 @@ export async function context(source, options) { ...options?.elements, }); - return serializer(moddleCtx, types, options?.extendFn); + return Serializer(moddleCtx, types, options?.extendFn); } export function moddleContext(source, options) { diff --git a/test/issues-test.js b/test/issues-test.js index b5ae0f9..24620b3 100644 --- a/test/issues-test.js +++ b/test/issues-test.js @@ -24,7 +24,7 @@ describe('issues', () => { const states = []; - listener.on('activity.start', (activity, engineApi) => { + listener.on('activity.start', (_activity, engineApi) => { states.push(engineApi.getState()); }); @@ -54,36 +54,43 @@ describe('issues', () => { expect(children.map(({ id }) => id)).to.eql(['Start', 'Parallel1', 'Task_A', 'Task_B', 'Parallel2', 'Task_C', 'End']); } + // eslint-disable-next-line no-useless-assignment let [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[0].definitions[0].execution.processes[0].execution.children; expect(Start, 'state 0 Start').to.have.property('status', 'started'); expect(Parallel1, 'state 0 Parallel1').to.not.have.property('status'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[1].definitions[0].execution.processes[0].execution.children; expect(Start, 'state 1 Start').to.have.property('status', 'end'); expect(Parallel1, 'state 1 Parallel1').to.have.property('status', 'started'); expect(Task_A, 'state 1 Task_A').to.not.have.property('status'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[2].definitions[0].execution.processes[0].execution.children; expect(Parallel1, 'state 2 Parallel1').to.have.property('status', 'end'); expect(Task_A, 'state 2 Task_A').to.have.property('status', 'started'); expect(Task_B, 'state 2 Task_B').to.not.have.property('status'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[3].definitions[0].execution.processes[0].execution.children; expect(Parallel1, 'state 3 Parallel1').to.have.property('status', 'end'); - expect(Task_A, 'state 3 Task_A').to.not.have.property('status'); - expect(Task_B, 'state 3 Task_B').to.have.property('status', 'started'); - expect(Parallel2, 'state 3 Parallel2').to.not.have.property('status'); + expect(Task_A, 'state 3 Task_A').to.have.property('status', 'end'); + expect(Task_B, 'state 3 Task_B').to.not.have.property('status'); + expect(Parallel2, 'state 3 Parallel2').to.have.property('status', 'started'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[4].definitions[0].execution.processes[0].execution.children; expect(Task_A, 'state 4 Task_A').to.not.have.property('status'); - expect(Task_B, 'state 4 Task_B').to.have.property('status', 'end'); - expect(Parallel2, 'state 4 Parallel2').to.have.property('status', 'started'); + expect(Task_B, 'state 4 Task_B').to.have.property('status', 'started'); + expect(Parallel2, 'state 4 Parallel2').to.have.property('status', 'executing'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[5].definitions[0].execution.processes[0].execution.children; expect(Parallel2, 'state 5 Parallel2').to.have.property('status', 'end'); expect(Task_C, 'state 5 Task_C').to.have.property('status', 'started'); expect(End, 'state 5 End').to.not.have.property('status'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[6].definitions[0].execution.processes[0].execution.children; expect(Task_C, 'state 6 Task_C').to.have.property('status', 'end'); expect(End, 'state 6 End').to.have.property('status', 'started'); diff --git a/test/resources/JsExtension.js b/test/resources/JsExtension.js index b3dd3be..f00e64c 100644 --- a/test/resources/JsExtension.js +++ b/test/resources/JsExtension.js @@ -13,9 +13,9 @@ export function extension(activity, context) { return { type: 'js:extension', extensions: { resultVariable, formKey }, - activate(msg) { - if (resultVariable) resultVariable.activate(msg); - if (formKey) formKey.activate(msg); + activate() { + if (resultVariable) resultVariable.activate(); + if (formKey) formKey.activate(); }, deactivate() { if (resultVariable) resultVariable.deactivate(); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..00678a7 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "include": ["**/*"], + "compilerOptions": { + "rootDir": "..", + "checkJs": true, + "emitDeclarationOnly": false, + "declaration": false, + "noEmit": true, + "noImplicitAny": false, + "types": ["node", "chai", "mocha"] + } +} diff --git a/tsconfig.json b/tsconfig.json index 8201c9f..454309b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,29 +1,23 @@ { - "include": ["src/**/*", "types"], + "include": ["src/**/*", "types/index.d.ts"], "compilerOptions": { - "emitDeclarationOnly": true, - "sourceMap": false, + "noEmit": true, "rootDir": "src", - "lib": ["es2017"], - "target": "es2017", - "outDir": "./tmp/ignore", - "declaration": true, - "noEmitOnError": true, + "lib": ["es2022"], + "target": "es2022", "noErrorTruncation": true, + "skipLibCheck": true, "allowJs": true, "checkJs": false, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": false, - "allowSyntheticDefaultImports": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "resolveJsonModule": true, "strict": true, + "strictNullChecks": false, "stripInternal": true, "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, - "typeRoots": ["./node_modules/@types"], - "paths": { - "types": ["./types/bpmn-engine.js"] - } + "types": ["node"] } } diff --git a/types/bpmn-engine.d.ts b/types/bpmn-engine.d.ts deleted file mode 100644 index d61a035..0000000 --- a/types/bpmn-engine.d.ts +++ /dev/null @@ -1,334 +0,0 @@ -// Author : Saeed Tabrizi -// Refactored by : Pål Edman - -import { EventEmitter } from 'node:events'; -import { Definitions as BpmnModdleDefinitions } from 'bpmn-moddle'; -import { extendFn, SerializableContext, TypeResolver } from 'moddle-context-serializer'; -import { - ActivityStatus, - ElementBroker, - EnvironmentOptions, - Definition, - DefinitionState, - Environment, - Api, - ElementBase, - ILogger, - EnvironmentState, - IScripts, - Script, -} from 'bpmn-elements'; - -declare module 'bpmn-engine' { - /** - * Engine emits the following events: - - error: An non-recoverable error has occurred - stop: Executions was stopped - end: Execution completed - */ - export type BpmnEngineEvent = 'error' | 'stop' | 'end'; - /** - * Each activity and flow emits events when changing state. - - activity.enter: An activity is entered - activity.start: An activity is started - activity.wait: The activity is postponed for some reason, e.g. a user task is waiting to be signaled or a message is expected - wait: Same as above - activity.end: An activity has ended successfully - activity.leave: The execution left the activity - activity.stop: Activity run was stopped - activity.throw: An recoverable error was thrown - activity.error: An non-recoverable error has occurred - */ - export type BpmnActivityEvent = - | 'activity.enter' - | 'activity.start' - | 'activity.wait' - | 'wait' - | 'activity.end' - | 'activity.leave' - | 'activity.stop' - | 'activity.throw' - | 'activity.error'; - /** - * Sequence flow events - flow.take: The sequence flow was taken - flow.discard: The sequence flow was discarded - flow.looped: The sequence is looped - */ - export type BpmnSequenceFlowEvent = 'flow.take' | 'flow.discard' | 'flow.looped'; - - export interface BpmnMessage { - id?: string; - executionId?: string; - [name: string]: any; - } - - export interface IListenerEmitter { - emit(eventName: string, ...args: any[]): void; - } - - export interface BpmnEngineExecuteOptions extends EnvironmentOptions { - listener?: EventEmitter | IListenerEmitter; - } - - export interface BpmnEngineOptions extends BpmnEngineExecuteOptions { - /** - * optional name of engine, - */ - name?: string; - /** - * optional BPMN 2.0 definition source as string - */ - source?: string; - /** - * optional serialized context supplied by moddle-context-serializer - */ - sourceContext?: SerializableContext; - /** - * optional override of BPMN Elements behaviors, defaults to bpmn-elements - */ - elements?: Record; - /** - * Passed to moddle-context-serializer as TypeResolver - */ - typeResolver?: typeof TypeResolver; - /** - * Passed to moddle-context-serializer as extendFn - */ - extendFn?: extendFn; - /** - * optional bpmn-moddle options to be passed to bpmn-moddle - */ - moddleOptions?: any; - moddleContext?: BpmnModdleDefinitions; - [x: string]: any; - } - - const enum BpmnEngineRunningStatus { - /** - * Engine is not executing - */ - Idle = 'idle', - /** - * Engine is executing - */ - Running = 'running', - /** - * Engine execution is stopped - */ - Stopped = 'stopped', - /** - * Engine execution has errored - */ - Error = 'error', - } - - export function Engine(options?: BpmnEngineOptions): Engine; - export class Engine extends EventEmitter { - constructor(options?: BpmnEngineOptions); - /** - * engine name - */ - readonly name: string; - /** - * engine broker - */ - broker: ElementBroker; - /** - * engine state - */ - readonly state: BpmnEngineRunningStatus; - /** - * boolean stopped - */ - readonly stopped: boolean; - /** - * current engine execution - */ - readonly execution: Execution; - /** - * engine environment - */ - readonly environment: Environment; - /** - * engine logger - */ - readonly logger: ILogger; - get activityStatus(): ActivityStatus; - - /** - * execute definition - * @param options Optional object with options to override the initial engine options - * @param cb Callback called before the throw - * @summary Execute options overrides the initial options passed to the engine before executing the definition. - */ - execute(): Promise; - execute(options: BpmnEngineExecuteOptions): Promise; - execute(options: BpmnEngineExecuteOptions, cb: (err: Error, execution?: Execution) => void): Promise; - execute(cb: (err: Error) => void): Promise; - - /** - * get definition by id - * @param id id of definition - */ - getDefinitionById(id: string): Promise; - - /** - * get all definitions - */ - getDefinitions(): Promise; - - /** - * get execution serialized state - */ - getState(): Promise; - - /** - * Recover engine from state - * @param state engine state - * @param recoverOptions optional object with options that will completely override the options passed to the engine at init - */ - recover(state: any, recoverOptions?: BpmnEngineOptions): Engine; - - /** - * Resume execution function with previously saved engine state. - * @param options - * @param callback - */ - resume(options?: BpmnEngineExecuteOptions, callback?: (err: Error, execution?: Execution) => void): Promise; - - /** - * Stop execution. The instance is terminated. - */ - stop(): Promise; - - waitFor(eventName: BpmnEngineEvent): Promise; - - /** - * Add definition source by source context. - * @param options - */ - addSource(options?: { sourceContext: any }): void; - } - - interface BpmnEngineDefinitionState extends DefinitionState { - /** - * Serialized moddle-context-serializer context - */ - source?: string; - } - - export interface BpmnEngineExecutionState { - name: string; - engineVersion: string; - state: BpmnEngineRunningStatus; - stopped: boolean; - environment: EnvironmentState; - definitions: BpmnEngineDefinitionState[]; - } - - export interface Execution { - /** - * engine name - * - * @type {string} - * @ - */ - readonly name: string; - /** - * state of execution, i.e running or idle - * - * @type {("running"| "idle")} - * - */ - readonly state: 'running' | 'idle'; - /** - * is the execution stopped - * - * @type {boolean} - * - */ - readonly stopped: boolean; - /** - * engine environment - * - * @type {Environment} - * - */ - readonly environment: Environment; - - /** - * executing definitions - * - * @type {Definition} - * - */ - readonly definitions: Definition[]; - get activityStatus(): ActivityStatus; - /** - * Are any definition running - */ - get isRunning(): boolean; - - /** - * Get activity/element by id. Loops the definitions and returns the first found activity with id. - * @param activityId Activity or element id - */ - getActivityById(activityId: string): ElementBase; - getActivityById(activityId: string): R; - - /** - * get execution serializable state - * - * @returns {BpmnEngineExecutionState} - * - */ - getState(): BpmnEngineExecutionState; - - /** - * stop execution - * - * - */ - stop(): Promise; - - /** - * get activities in a postponed state - * - * - */ - getPostponed(): Api[]; - - /** - * send signal to execution, distributed to all definitions - * Delegate a signal message to all interested parties, usually MessageEventDefinition, SignalEventDefinition, SignalTask (user, manual), ReceiveTask, or a StartEvent that has a form. - * @param message {BpmnMessage} - * @param options - */ - signal(message?: BpmnMessage, options?: { ignoreSameDefinition?: boolean }): void; - - /** - * send cancel activity to execution, distributed to all definitions - * Delegate a cancel message to all interested parties, perhaps a stalled TimerEventDefinition. - * @param message - */ - cancelActivity(message?: BpmnMessage): void; - - /** - * - * @param event - */ - waitFor(event: BpmnEngineEvent): Promise; - } - - export class JavaScripts implements IScripts { - /** - * @param disableDummy Disable returning dummy scripts if script is not found. Dummy script will immediately call next function without error or return value - */ - constructor(disableDummy?: boolean); - register(activity: any): Script | undefined; - getScript(language: string, identifier: { id: string; [x: string]: any }): Script; - } -} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..2970491 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,221 @@ +declare module 'bpmn-engine' { + import type { EventEmitter } from 'node:events'; + import type { Broker } from 'smqp'; + import type { Definitions as BpmnModdleDefinitions } from 'bpmn-moddle'; + import type { ExtendFn, SerializableContext, TypeResolverExtender } from 'moddle-context-serializer'; + import type { + ActivityStatus, + Definition, + ElementBase, + Environment, + EnvironmentOptions, + EnvironmentState, + IApi, + ILogger, + IScripts, + Script, + } from 'bpmn-elements'; + + /** + * Engine events. + * - `error`: A non-recoverable error has occurred + * - `stop`: Execution was stopped + * - `end`: Execution completed + */ + export type BpmnEngineEvent = 'error' | 'stop' | 'end'; + + /** + * Activity events emitted to the listener. + * - `activity.enter`: An activity is entered + * - `activity.start`: An activity is started + * - `activity.wait`: The activity is postponed for some reason, e.g. a user task is waiting to be signaled or a message is expected + * - `wait`: Same as above + * - `activity.timer`: A timer was started + * - `activity.timeout`: A timer timed out + * - `activity.signal`: The activity was signaled + * - `activity.catch`: The activity caught a message, signal, error, etc + * - `activity.discard`: The activity was discarded + * - `activity.cancel`: The activity was cancelled + * - `activity.end`: An activity has ended successfully + * - `activity.leave`: The execution left the activity + * - `activity.stop`: Activity run was stopped + * - `activity.throw`: A recoverable error was thrown + * - `activity.error`: A non-recoverable error has occurred + */ + export type BpmnActivityEvent = + | 'activity.enter' + | 'activity.start' + | 'activity.wait' + | 'wait' + | 'activity.timer' + | 'activity.timeout' + | 'activity.signal' + | 'activity.catch' + | 'activity.discard' + | 'activity.cancel' + | 'activity.end' + | 'activity.leave' + | 'activity.stop' + | 'activity.throw' + | 'activity.error'; + + /** + * Sequence flow events emitted to the listener. + * - `flow.take`: The sequence flow was taken + * + * Since bpmn-elements@18 non-selected sequence flows are no longer discarded, so + * `flow.discard` and `flow.looped` are never emitted during execution. + */ + export type BpmnSequenceFlowEvent = 'flow.take'; + + /** + * Process events emitted to the listener. + */ + export type BpmnProcessEvent = + 'process.enter' | 'process.end' | 'process.leave' | 'process.error' | 'process.terminate' | 'process.discarded'; + + /** + * Definition events emitted to the listener. + */ + export type BpmnDefinitionEvent = 'definition.leave' | 'definition.stop' | 'definition.error'; + + /** + * All events emitted to the listener. Every `definition.#`, `process.#`, `activity.#`, and `flow.#` + * routing key published by bpmn-elements is forwarded verbatim, the named unions are the documented ones. + */ + export type BpmnListenerEvent = + | BpmnDefinitionEvent + | BpmnProcessEvent + | BpmnActivityEvent + | BpmnSequenceFlowEvent + | `definition.${string}` + | `process.${string}` + | `activity.${string}` + | `flow.${string}`; + + export type BpmnEngineRunningStatus = 'idle' | 'running' | 'stopped' | 'error'; + + export interface BpmnMessage { + id?: string; + executionId?: string; + [name: string]: any; + } + + export interface IListenerEmitter { + /** + * Receive engine execution events + * @param eventName routing key of the event + * @param elementApi api of the element that emitted the event + * @param execution the engine execution + */ + emit(eventName: BpmnListenerEvent, elementApi: IApi, execution: Execution): unknown; + emit(eventName: string, ...args: any[]): unknown; + } + + export interface BpmnEngineExecuteOptions extends EnvironmentOptions { + listener?: EventEmitter | IListenerEmitter; + } + + export interface BpmnEngineOptions extends BpmnEngineExecuteOptions { + name?: string; + source?: string | Buffer; + sourceContext?: SerializableContext; + elements?: Record; + typeResolver?: TypeResolverExtender; + extendFn?: ExtendFn; + moddleOptions?: any; + moddleContext?: BpmnModdleDefinitions; + Logger?: (scope: string) => ILogger; + scripts?: IScripts; + disableDummyScript?: boolean; + listener?: EventEmitter | IListenerEmitter; + [x: string]: any; + } + + export interface BpmnEngineDefinitionState { + state: string; + source?: string; + [x: string]: any; + } + + export interface BpmnEngineExecutionState { + name?: string; + engineVersion?: string; + state?: BpmnEngineRunningStatus; + stopped?: boolean; + environment?: EnvironmentState; + definitions?: BpmnEngineDefinitionState[]; + } + + export function Engine(options?: BpmnEngineOptions): Engine; + export class Engine extends EventEmitter { + constructor(options?: BpmnEngineOptions); + options: BpmnEngineOptions; + name: string; + readonly broker: Broker; + readonly logger: ILogger; + readonly environment: Environment; + readonly state: BpmnEngineRunningStatus; + readonly stopped: boolean; + readonly execution: Execution | null; + readonly activityStatus: ActivityStatus; + + execute(): Promise; + execute(options: BpmnEngineExecuteOptions): Promise; + execute(options: BpmnEngineExecuteOptions, callback: (err: Error | null, execution?: Execution) => void): Promise; + execute(callback: (err: Error | null, execution?: Execution) => void): Promise; + + getDefinitionById(id: string): Promise; + getDefinitions(executeOptions?: BpmnEngineExecuteOptions): Promise; + getState(): Promise; + + recover(savedState?: BpmnEngineExecutionState | null, recoverOptions?: BpmnEngineOptions): Engine; + + resume(): Promise; + resume(options: BpmnEngineExecuteOptions): Promise; + resume(options: BpmnEngineExecuteOptions, callback: (err: Error | null, execution?: Execution) => void): Promise; + resume(callback: (err: Error | null, execution?: Execution) => void): Promise; + + stop(): Promise | undefined; + + addSource(options?: { sourceContext: SerializableContext }): void; + + waitFor(eventName: BpmnEngineEvent | string): Promise; + } + + export class Execution { + constructor(engine: Engine, definitions: Definition[], options?: BpmnEngineExecuteOptions, isRecovered?: boolean); + options: BpmnEngineExecuteOptions; + readonly name: string; + readonly definitions: Definition[]; + readonly broker: Broker; + readonly environment: Environment; + readonly state: BpmnEngineRunningStatus; + readonly stopped: boolean; + readonly isRunning: boolean; + readonly activityStatus: ActivityStatus; + + getActivityById(activityId: string): ElementBase | undefined; + getActivityById(activityId: string): R | undefined; + getPostponed(): IApi[]; + getState(): BpmnEngineExecutionState; + stop(): Promise; + + signal(message?: BpmnMessage, options?: { ignoreSameDefinition?: boolean }): void; + cancelActivity(message?: BpmnMessage): void; + + waitFor(eventName: BpmnEngineEvent | string): Promise; + } + + export interface JavaScripts extends IScripts { + register(activity: any): Script | undefined; + getScript(language: string, identifier: { id: string; [x: string]: any }): Script | undefined; + } + export const JavaScripts: { + new (disableDummy?: boolean): JavaScripts; + /** callable without new */ + (disableDummy?: boolean): JavaScripts; + }; + + export default Engine; +}