From 98a89a33911b7243a6ea6ec80703f6b8abd11378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Fri, 5 Dec 2025 06:02:51 +0100 Subject: [PATCH 1/6] change v headline --- CHANGELOG.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e7702..cd490fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ # Changelog -## [25.0.1] - 2025-11-14 +## 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 +17,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 +66,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 +78,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. From 85c00033fd4eedd14bfdd3f39cb5518ca69319e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sat, 13 Jun 2026 08:44:15 +0200 Subject: [PATCH 2/6] upgrade bpmn-elements@18 --- .github/workflows/build.yaml | 4 +- .github/workflows/release.yaml | 2 +- .gitignore | 4 + .nvmrc | 2 +- AGENTS.md | 65 +++++++ CHANGELOG.md | 12 ++ docs/API.md | 17 +- docs/Examples.md | 20 +- eslint.config.js | 13 +- lib/index.cjs | 99 +++++++++- package.json | 27 +-- scripts/generate-api-toc.js | 126 ++++++++++--- src/index.js | 84 ++++++++- test/feature/extend-feature.js | 6 +- test/feature/issues-feature.js | 72 ++++--- test/helpers/testHelpers.js | 4 +- test/issues-test.js | 17 +- tsconfig.json | 24 +-- types/bpmn-engine.d.ts | 334 --------------------------------- types/index.d.ts | 153 +++++++++++++++ types/interfaces.d.ts | 80 ++++++++ 21 files changed, 697 insertions(+), 468 deletions(-) create mode 100644 AGENTS.md delete mode 100644 types/bpmn-engine.d.ts create mode 100644 types/index.d.ts create mode 100644 types/interfaces.d.ts diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 04dc54c..6a4651f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,10 +11,10 @@ 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@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a9dc6c5..f42f20b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,7 +13,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: 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/.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..acb241e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,65 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +`.nvmrc` pins Node 18; `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). `posttest` then runs `lint`, `toc` (regenerates API doc TOCs), and `dist`. +- `npm run wintest` — same suite, plain `mocha` (no `-p`/parallel; for environments where worker pools misbehave). +- `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 `docs/API.md` and `docs/Examples.md` via the in-repo `scripts/generate-api-toc.js` (no `markdown-toc` dep). It takes the markdown files as a comma- or space-separated argument, rewrites the block between `` and ``, skips the first H1 (the doc title — `docs/API.md` keeps a plain `# API Reference` title for this), and disambiguates duplicate anchors GitHub-style (`#getstate`, `#getstate-1`). +- `npm run test-md` — runs `texample` against `docs/API.md`, `docs/Examples.md`, `docs/Upgrade.md`. **Not** in the regular `posttest` chain; run manually when API doc examples change. +- `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. +- **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) with shared interface definitions in `types/interfaces.d.ts` (referenced by `src` JSDoc via the tsconfig `types` path). +- `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 cd490fd..02a152f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## v26.0.0 - 2026-06-13 + +### Breaking + +- major update [`bpmn-elements@18`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) — non-selected gateway flows are no longer discarded +- update to [`moddle-context-serializer@6`](https://github.com/paed01/moddle-context-serializer/blob/master/CHANGELOG.md) +- require `smqp@>=13` peer dependency + +### Other + +- ship hand-maintained TypeScript declarations + ## v25.0.1 - 2025-11-14 - provenance release to get green badge next to npm package version diff --git a/docs/API.md b/docs/API.md index 68e3d08..490537d 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) @@ -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 diff --git a/docs/Examples.md b/docs/Examples.md index 0912045..2bbbbce 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -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/eslint.config.js b/eslint.config.js index 9ce23c5..57280c9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -74,7 +74,7 @@ export default [ languageOptions: { parserOptions: { sourceType: 'module', - ecmaVersion: 2020, + ecmaVersion: 2025, }, }, rules, @@ -87,6 +87,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..73d756f 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -8,7 +8,7 @@ var node_url = require('node:url'); 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'); @@ -161,6 +161,17 @@ ProcessOutputDataObject.prototype.write = function writeDataObject(broker, excha return broker.publish(exchange, `${routingKeyPrefix}response`, { id, name, type, value }, messageProperties); }; +/** + * @typedef {import('types').BpmnEngineOptions} BpmnEngineOptions + * @typedef {import('types').BpmnEngineExecuteOptions} BpmnEngineExecuteOptions + * @typedef {import('types').BpmnEngineExecutionState} BpmnEngineExecutionState + * @typedef {import('types').BpmnEngineEvent} BpmnEngineEvent + * @typedef {import('types').BpmnEngineRunningStatus} BpmnEngineRunningStatus + * @typedef {import('types').BpmnMessage} BpmnMessage + * @typedef {import('types').SerializableContext} SerializableContext + */ + + 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'); @@ -175,6 +186,10 @@ const kState = Symbol.for('state'); const kStopped = Symbol.for('stopped'); const kTypeResolver = Symbol.for('type resolver'); +/** + * BPMN 2.0 execution engine. + * @param {import('types').BpmnEngineOptions} [options] + */ function Engine(options) { if (!(this instanceof Engine)) return new Engine(options); @@ -188,7 +203,7 @@ function Engine(options) { this.logger = opts.Logger('engine'); - this[kTypeResolver] = serializer.TypeResolver( + this[kTypeResolver] = moddleContextSerializer.TypeResolver( { ...Elements__namespace, ...opts.elements, @@ -260,6 +275,12 @@ Object.defineProperties(Engine.prototype, { }, }); +/** + * Execute the loaded definitions. + * @param {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 +294,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 {BpmnEngineExecutionState | null} savedState + * @param {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 +334,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 +352,12 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { return this; }; +/** + * Resume execution from a previously recovered state. + * @param {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 +379,10 @@ Engine.prototype.resume = async function resume(...args) { return execution._resume(resumeOptions, callback); }; +/** + * Add a pre-serialized source context to the engine. + * @param {{ sourceContext: SerializableContext }} [options] + */ Engine.prototype.addSource = function addSource(options) { if (!options?.sourceContext) return; const loadedDefinitions = this[kLoadedDefinitions]; @@ -352,16 +390,25 @@ Engine.prototype.addSource = function addSource(options) { this[kPendingSources].add(options.sourceContext); }; +/** + * @param {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 +417,11 @@ Engine.prototype.getState = async function getState() { return new Execution(this, definitions, this.options).getState(); }; +/** + * @template R + * @param {BpmnEngineEvent} eventName + * @returns {Promise} + */ Engine.prototype.waitFor = function waitFor(eventName) { const self = this; return new Promise((resolve, reject) => { @@ -387,12 +439,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 +469,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()); }; +/** + * @param {Engine} engine + * @param {any[]} definitions + * @param {BpmnEngineExecuteOptions} [options] + * @param {boolean} [isRecovered] + */ function Execution(engine, definitions, options, isRecovered = false) { this.name = engine.name; this.options = options; @@ -478,6 +541,7 @@ Object.defineProperties(Execution.prototype, { }, }); +/** @internal */ Execution.prototype._execute = function execute(executeOptions, callback) { this._setup(executeOptions); this[kStopped] = false; @@ -499,6 +563,7 @@ Execution.prototype._execute = function execute(executeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._resume = function resume(resumeOptions, callback) { this._setup(resumeOptions); @@ -512,6 +577,7 @@ Execution.prototype._resume = function resume(resumeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._addConsumerCallbacks = function addConsumerCallbacks(callback) { if (!callback) return; @@ -562,6 +628,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 +646,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 +666,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 +736,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 +744,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 +766,7 @@ Execution.prototype._saveOutput = function saveOutput(output) { } }; +/** @returns {BpmnEngineExecutionState} */ Execution.prototype.getState = function getState() { const definitions = []; for (const definition of this.definitions) { @@ -713,6 +786,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 +797,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 +807,10 @@ Execution.prototype.getPostponed = function getPostponed() { return result; }; +/** + * @param {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 +818,35 @@ Execution.prototype.signal = function signal(payload, signalOptions) { } }; +/** @param {BpmnMessage} [payload] */ Execution.prototype.cancelActivity = function cancelActivity(payload) { for (const definition of this[kExecuting]) { definition.cancelActivity(payload); } }; +/** + * @template T + * @param {BpmnEngineEvent} 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..fc601ad 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.0", "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" } @@ -59,29 +59,30 @@ ], "devDependencies": { "@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/node": "^20.19.40", "bent": "^7.3.12", - "c8": "^10.0.0", + "c8": "^11.0.0", "camunda-bpmn-moddle": "^7.0.1", "chai": "^6.2.0", - "chronokinesis": "^7.0.1", - "eslint": "^9.10.0", - "markdown-toc": "^1.2.0", + "chronokinesis": "^8.0.0", + "eslint": "^10.5.0", + "globals": "^17.6.0", "mocha": "^11.0.1", "mocha-cakes-2": "^3.3.0", "prettier": "^3.2.5", "rollup": "^4.10.0", - "texample": "^0.1.0" + "texample": "^1.0.2" }, "dependencies": { - "bpmn-elements": "^17.2.2", + "bpmn-elements": "^18.0.0", "bpmn-moddle": "^9.0.4", "debug": "^4.4.3", - "moddle-context-serializer": "^4.4.1" + "moddle-context-serializer": "^6.0.0" }, "peerDependencies": { - "smqp": ">=9" + "smqp": "^13.0.1" } } diff --git a/scripts/generate-api-toc.js b/scripts/generate-api-toc.js index 108cb3b..daf4666 100644 --- a/scripts/generate-api-toc.js +++ b/scripts/generate-api-toc.js @@ -1,36 +1,106 @@ -import fs from 'node:fs'; -import { createRequire } from 'node:module'; +#!/usr/bin/env node +/* eslint-disable no-console */ +import { readFile, writeFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; -import Toc from 'markdown-toc'; +import { dirname, resolve } from 'node:path'; -const nodeRequire = createRequire(fileURLToPath(import.meta.url)); -const { version } = nodeRequire('../package.json'); +const TOC_START = ''; +const TOC_END = ''; -const filenames = getFileNames(); +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(here, '..'); -function getFileNames() { - const arg = process.argv[2] || './API.md'; - return arg.split(','); +const args = process.argv.slice(2); +const files = (args.length ? args : ['README.md']).flatMap((arg) => arg.split(',')).filter(Boolean); + +let exitCode = 0; +for (const arg of files) { + try { + await processFile(resolve(repoRoot, arg)); + } catch (err) { + console.error(`${arg}: ${err.message}`); + exitCode = 1; + } } +process.exit(exitCode); + +/** @param {string} mdPath */ +async function processFile(mdPath) { + const source = await readFile(mdPath, 'utf8'); + + const inFence = (() => { + const fences = []; + let open = false; + for (const [i, line] of source.split('\n').entries()) { + if (/^```/.test(line)) { + open = !open; + } + fences[i] = open; + } + return (i) => fences[i]; + })(); + + const lines = source.split('\n'); + /** @type {Array<{ level: number, text: string, slug: string }>} */ + const headlines = []; + const slugCounts = new Map(); + let firstHeadlineIdx = -1; + let titleSkipped = false; + + for (const [i, line] of lines.entries()) { + if (inFence(i)) continue; + const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line); + if (!match) continue; + const level = match[1].length; + if (level === 1 && !titleSkipped) { + titleSkipped = true; + continue; + } + const text = match[2].replace(/\*/g, '\\*'); + const baseSlug = slugify(match[2]); + const n = slugCounts.get(baseSlug) ?? 0; + slugCounts.set(baseSlug, n + 1); + const slug = n === 0 ? baseSlug : `${baseSlug}-${n}`; + headlines.push({ level, text, slug }); + if (firstHeadlineIdx === -1) firstHeadlineIdx = i; + } -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); + if (firstHeadlineIdx === -1 || headlines.length === 0) { + console.error(`${mdPath}: no non-title headlines found; nothing to do.`); + return; + } + + const minLevel = Math.min(...headlines.map((h) => h.level)); + const tocLines = headlines.map(({ level, text, slug }) => `${' '.repeat(level - minLevel)}- [${text}](#${slug})`); + const tocBlock = [TOC_START, '', ...tocLines, '', TOC_END].join('\n'); + + const startIdx = lines.findIndex((l) => l.trim() === TOC_START); + const endIdx = lines.findIndex((l) => l.trim() === TOC_END); + + let updated; + if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { + updated = [...lines.slice(0, startIdx), tocBlock, ...lines.slice(endIdx + 1)].join('\n'); + } else { + updated = [...lines.slice(0, firstHeadlineIdx), tocBlock, '', ...lines.slice(firstHeadlineIdx)].join('\n'); + } + + if (updated === source) { + console.log(`${mdPath}: TOC already up to date.`); + } else { + await writeFile(mdPath, updated); + console.log(`${mdPath}: wrote TOC with ${headlines.length} entries.`); + } } -filenames.forEach(generate); +/** + * @param {string} text + * @returns {string} + */ +function slugify(text) { + return text + .toLowerCase() + .replace(/[`*_~]/g, '') + .replace(/[^\w\s-]/g, '') + .trim() + .replace(/\s+/g, '-'); +} diff --git a/src/index.js b/src/index.js index 82b69dc..3c53c4c 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url'; import 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'; @@ -29,6 +29,10 @@ const kTypeResolver = Symbol.for('type resolver'); export default Engine; export { JavaScripts }; +/** + * BPMN 2.0 execution engine. + * @param {import('types').BpmnEngineOptions} [options] + */ export function Engine(options) { if (!(this instanceof Engine)) return new Engine(options); @@ -114,6 +118,12 @@ Object.defineProperties(Engine.prototype, { }, }); +/** + * Execute the loaded definitions. + * @param {import('types').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 +137,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('types').BpmnEngineExecutionState | null} savedState + * @param {import('types').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 +195,12 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { return this; }; +/** + * Resume execution from a previously recovered state. + * @param {import('types').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 +222,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('types').SerializableContext }} [options] + */ Engine.prototype.addSource = function addSource(options) { if (!options?.sourceContext) return; const loadedDefinitions = this[kLoadedDefinitions]; @@ -206,16 +233,25 @@ Engine.prototype.addSource = function addSource(options) { this[kPendingSources].add(options.sourceContext); }; +/** + * @param {import('types').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 +260,11 @@ Engine.prototype.getState = async function getState() { return new Execution(this, definitions, this.options).getState(); }; +/** + * @template R + * @param {import('types').BpmnEngineEvent} eventName + * @returns {Promise} + */ Engine.prototype.waitFor = function waitFor(eventName) { const self = this; return new Promise((resolve, reject) => { @@ -241,12 +282,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 +312,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()); }; +/** + * @param {Engine} engine + * @param {any[]} definitions + * @param {import('types').BpmnEngineExecuteOptions} [options] + * @param {boolean} [isRecovered] + */ export function Execution(engine, definitions, options, isRecovered = false) { this.name = engine.name; this.options = options; @@ -332,6 +384,7 @@ Object.defineProperties(Execution.prototype, { }, }); +/** @internal */ Execution.prototype._execute = function execute(executeOptions, callback) { this._setup(executeOptions); this[kStopped] = false; @@ -353,6 +406,7 @@ Execution.prototype._execute = function execute(executeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._resume = function resume(resumeOptions, callback) { this._setup(resumeOptions); @@ -366,6 +420,7 @@ Execution.prototype._resume = function resume(resumeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._addConsumerCallbacks = function addConsumerCallbacks(callback) { if (!callback) return; @@ -416,6 +471,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 +489,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 +509,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 +579,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 +587,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 +609,7 @@ Execution.prototype._saveOutput = function saveOutput(output) { } }; +/** @returns {import('types').BpmnEngineExecutionState} */ Execution.prototype.getState = function getState() { const definitions = []; for (const definition of this.definitions) { @@ -567,6 +629,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 +640,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 +650,10 @@ Execution.prototype.getPostponed = function getPostponed() { return result; }; +/** + * @param {import('types').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 +661,35 @@ Execution.prototype.signal = function signal(payload, signalOptions) { } }; +/** @param {import('types').BpmnMessage} [payload] */ Execution.prototype.cancelActivity = function cancelActivity(payload) { for (const definition of this[kExecuting]) { definition.cancelActivity(payload); } }; +/** + * @template T + * @param {import('types').BpmnEngineEvent} 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/feature/extend-feature.js b/test/feature/extend-feature.js index 0ef1c86..96b25e0 100644 --- a/test/feature/extend-feature.js +++ b/test/feature/extend-feature.js @@ -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); }); }); diff --git a/test/feature/issues-feature.js b/test/feature/issues-feature.js index 2c27a14..e34cc7e 100644 --- a/test/feature/issues-feature.js +++ b/test/feature/issues-feature.js @@ -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 }); }); }); @@ -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', () => { diff --git a/test/helpers/testHelpers.js b/test/helpers/testHelpers.js index fabc45c..03e2596 100644 --- a/test/helpers/testHelpers.js +++ b/test/helpers/testHelpers.js @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; 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 +25,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..0c92e7e 100644 --- a/test/issues-test.js +++ b/test/issues-test.js @@ -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/tsconfig.json b/tsconfig.json index 8201c9f..115d6b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,29 +1,25 @@ { - "include": ["src/**/*", "types"], + "include": ["src/**/*", "types/interfaces.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": ["./types/interfaces.d.ts"] } } } 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..935a6cd --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,153 @@ +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, ResolverFn } from 'moddle-context-serializer'; + import type { + ActivityStatus, + Definition, + ElementBase, + Environment, + EnvironmentOptions, + EnvironmentState, + IApi, + ILogger, + IScripts, + Script, + } from 'bpmn-elements'; + + export type BpmnEngineEvent = 'error' | 'stop' | 'end'; + + export type BpmnActivityEvent = + | 'activity.enter' + | 'activity.start' + | 'activity.wait' + | 'wait' + | 'activity.end' + | 'activity.leave' + | 'activity.stop' + | 'activity.throw' + | 'activity.error'; + + export type BpmnSequenceFlowEvent = 'flow.take' | 'flow.discard' | 'flow.looped'; + + export type BpmnEngineRunningStatus = 'idle' | 'running' | 'stopped' | 'error'; + + 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 { + name?: string; + source?: string; + sourceContext?: SerializableContext; + elements?: Record; + typeResolver?: ResolverFn; + extendFn?: ExtendFn; + moddleOptions?: any; + moddleContext?: BpmnModdleDefinitions; + Logger?: (scope: string) => ILogger; + scripts?: IScripts; + disableDummyScript?: boolean; + [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; + readonly 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): Promise; + } + + export function Execution( + engine: Engine, + definitions: Definition[], + options?: BpmnEngineExecuteOptions, + isRecovered?: boolean + ): Execution; + 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): Promise; + } + + export class JavaScripts implements IScripts { + constructor(disableDummy?: boolean); + register(activity: any): Script | undefined; + getScript(language: string, identifier: { id: string; [x: string]: any }): Script; + } + + export default Engine; +} diff --git a/types/interfaces.d.ts b/types/interfaces.d.ts new file mode 100644 index 0000000..ee93701 --- /dev/null +++ b/types/interfaces.d.ts @@ -0,0 +1,80 @@ +import type { EventEmitter } from 'node:events'; +import type { Definitions as BpmnModdleDefinitions } from 'bpmn-moddle'; +import type { ExtendFn, SerializableContext, ResolverFn } from 'moddle-context-serializer'; +import type { ActivityStatus, ElementBroker, EnvironmentOptions, EnvironmentState, IScripts, Script, ILogger } from 'bpmn-elements'; + +export type { SerializableContext }; + +export type BpmnEngineEvent = 'error' | 'stop' | 'end'; + +export type BpmnActivityEvent = + | 'activity.enter' + | 'activity.start' + | 'activity.wait' + | 'wait' + | 'activity.end' + | 'activity.leave' + | 'activity.stop' + | 'activity.throw' + | 'activity.error'; + +export type BpmnSequenceFlowEvent = 'flow.take' | 'flow.discard' | 'flow.looped'; + +export type BpmnEngineRunningStatus = 'idle' | 'running' | 'stopped' | 'error'; + +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 { + name?: string; + source?: string; + sourceContext?: SerializableContext; + elements?: Record; + typeResolver?: ResolverFn; + extendFn?: ExtendFn; + moddleOptions?: any; + moddleContext?: BpmnModdleDefinitions; + Logger?: (scope: string) => ILogger; + scripts?: IScripts; + disableDummyScript?: boolean; + [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 type { + ActivityStatus, + BpmnModdleDefinitions, + ElementBroker, + EnvironmentOptions, + EnvironmentState, + ExtendFn, + ILogger, + IScripts, + ResolverFn, + Script, +}; From 1ac92cda68a60eaec8b6c0928c2ba77b983b7aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sat, 13 Jun 2026 08:47:42 +0200 Subject: [PATCH 3/6] yaaaargs --- lib/index.cjs | 41 +++++++++++++++-------------------------- package.json | 8 ++++++++ 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/index.cjs b/lib/index.cjs index 73d756f..b61029e 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -161,17 +161,6 @@ ProcessOutputDataObject.prototype.write = function writeDataObject(broker, excha return broker.publish(exchange, `${routingKeyPrefix}response`, { id, name, type, value }, messageProperties); }; -/** - * @typedef {import('types').BpmnEngineOptions} BpmnEngineOptions - * @typedef {import('types').BpmnEngineExecuteOptions} BpmnEngineExecuteOptions - * @typedef {import('types').BpmnEngineExecutionState} BpmnEngineExecutionState - * @typedef {import('types').BpmnEngineEvent} BpmnEngineEvent - * @typedef {import('types').BpmnEngineRunningStatus} BpmnEngineRunningStatus - * @typedef {import('types').BpmnMessage} BpmnMessage - * @typedef {import('types').SerializableContext} SerializableContext - */ - - 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'); @@ -277,7 +266,7 @@ Object.defineProperties(Engine.prototype, { /** * Execute the loaded definitions. - * @param {BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {import('types').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] * @param {(err: Error, execution?: Execution) => void} [callback] * @returns {Promise} */ @@ -303,8 +292,8 @@ Engine.prototype.stop = function stop() { /** * Recover engine from saved state. - * @param {BpmnEngineExecutionState | null} savedState - * @param {BpmnEngineOptions} [recoverOptions] + * @param {import('types').BpmnEngineExecutionState | null} savedState + * @param {import('types').BpmnEngineOptions} [recoverOptions] * @returns {Engine} */ Engine.prototype.recover = function recover(savedState, recoverOptions) { @@ -354,7 +343,7 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { /** * Resume execution from a previously recovered state. - * @param {BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {import('types').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] * @param {(err: Error, execution?: Execution) => void} [callback] * @returns {Promise} */ @@ -381,7 +370,7 @@ Engine.prototype.resume = async function resume(...args) { /** * Add a pre-serialized source context to the engine. - * @param {{ sourceContext: SerializableContext }} [options] + * @param {{ sourceContext: import('types').SerializableContext }} [options] */ Engine.prototype.addSource = function addSource(options) { if (!options?.sourceContext) return; @@ -391,8 +380,8 @@ Engine.prototype.addSource = function addSource(options) { }; /** - * @param {BpmnEngineExecuteOptions} [executeOptions] - * @returns {Promise} + * @param {import('types').BpmnEngineExecuteOptions} [executeOptions] + * @returns {Promise} */ Engine.prototype.getDefinitions = function getDefinitions(executeOptions) { const loadedDefinitions = this[kLoadedDefinitions]; @@ -402,13 +391,13 @@ Engine.prototype.getDefinitions = function getDefinitions(executeOptions) { /** * @param {string} id - * @returns {Promise} + * @returns {Promise} */ Engine.prototype.getDefinitionById = async function getDefinitionById(id) { return (await this.getDefinitions()).find((d) => d.id === id); }; -/** @returns {Promise} */ +/** @returns {Promise} */ Engine.prototype.getState = async function getState() { const execution = this.execution; if (execution) return execution.getState(); @@ -419,7 +408,7 @@ Engine.prototype.getState = async function getState() { /** * @template R - * @param {BpmnEngineEvent} eventName + * @param {import('types').BpmnEngineEvent} eventName * @returns {Promise} */ Engine.prototype.waitFor = function waitFor(eventName) { @@ -489,7 +478,7 @@ Engine.prototype._getModdleContext = function getModdleContext(source) { /** * @param {Engine} engine * @param {any[]} definitions - * @param {BpmnEngineExecuteOptions} [options] + * @param {import('types').BpmnEngineExecuteOptions} [options] * @param {boolean} [isRecovered] */ function Execution(engine, definitions, options, isRecovered = false) { @@ -766,7 +755,7 @@ Execution.prototype._saveOutput = function saveOutput(output) { } }; -/** @returns {BpmnEngineExecutionState} */ +/** @returns {import('types').BpmnEngineExecutionState} */ Execution.prototype.getState = function getState() { const definitions = []; for (const definition of this.definitions) { @@ -808,7 +797,7 @@ Execution.prototype.getPostponed = function getPostponed() { }; /** - * @param {BpmnMessage} [payload] + * @param {import('types').BpmnMessage} [payload] * @param {{ ignoreSameDefinition?: boolean }} [signalOptions] */ Execution.prototype.signal = function signal(payload, signalOptions) { @@ -818,7 +807,7 @@ Execution.prototype.signal = function signal(payload, signalOptions) { } }; -/** @param {BpmnMessage} [payload] */ +/** @param {import('types').BpmnMessage} [payload] */ Execution.prototype.cancelActivity = function cancelActivity(payload) { for (const definition of this[kExecuting]) { definition.cancelActivity(payload); @@ -827,7 +816,7 @@ Execution.prototype.cancelActivity = function cancelActivity(payload) { /** * @template T - * @param {BpmnEngineEvent} eventName + * @param {import('types').BpmnEngineEvent} eventName * @returns {Promise} */ Execution.prototype.waitFor = function waitFor(...args) { diff --git a/package.json b/package.json index fc601ad..bcfd625 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,14 @@ "url": "https://github.com/paed01/bpmn-engine/master/LICENSE" } ], + "overrides": { + "c8": { + "yargs": "^18.0.0" + }, + "mocha": { + "yargs": "^18.0.0" + } + }, "devDependencies": { "@bonniernews/hot-bev": "^0.4.0", "@eslint/js": "^10.0.1", From a849aa5f960c9b2b5da0952845751be028314231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sat, 13 Jun 2026 08:58:18 +0200 Subject: [PATCH 4/6] bump appveyor builds --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 4871d7f..c4d9d6e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ environment: matrix: - - nodejs_version: '18' - nodejs_version: '20' + - nodejs_version: '22' platform: - x64 From b4a9f955ee781290a9591907ae737b337c0f3839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sat, 13 Jun 2026 09:29:14 +0200 Subject: [PATCH 5/6] appveyor yaaargs! --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index c4d9d6e..47b2a20 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,5 @@ environment: matrix: - - nodejs_version: '20' - nodejs_version: '22' platform: From 80ccd20619902195df456db9680823a887bc7aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Tue, 7 Jul 2026 10:17:22 +0200 Subject: [PATCH 6/6] move deps to peer, multiple versions are supported --- .mocharc.json | 1 + AGENTS.md | 2 +- CHANGELOG.md | 5 + README.md | 30 ++++++ docs/bpmn-moddle.md | 48 +++++++++ eslint.config.js | 5 +- package.json | 17 ++-- test/Engine-test.js | 1 + ...n-moddle-backward-compatibility-feature.js | 97 +++++++++++++++++++ test/feature/extend-feature.js | 2 +- test/feature/issues-feature.js | 4 +- 11 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 docs/bpmn-moddle.md create mode 100644 test/feature/bpmn-moddle-backward-compatibility-feature.js diff --git a/.mocharc.json b/.mocharc.json index c6c2062..3a6a52d 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -3,5 +3,6 @@ "reporter": "spec", "timeout": 1000, "ui": "mocha-cakes-2", + "spec": ["test/**/*-feature.js", "test/**/*-test.js"], "require": ["chai/register-expect.js"] } diff --git a/AGENTS.md b/AGENTS.md index acb241e..e7f5a67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,7 +43,7 @@ smqp (peer dep) ── message broker driving execution - **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. +- **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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 02a152f..42b839c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v26.0.1 - 2026-07-07 + +- declare `bpmn-elements`, `bpmn-moddle`, `moddle-context-serializer`, and `debug` as peer dependencies (install and pin them yourself) +- verify `bpmn-moddle` 9 and 10 both work — see [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) + ## v26.0.0 - 2026-06-13 ### Breaking diff --git a/README.md b/README.md index 323f4c7..67ce2e6 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,47 @@ BPMN 2.0 execution engine. Open source javascript workflow engine. - [Changelog](/CHANGELOG.md) - [Examples](/docs/Examples.md) - [Upgrade version](/docs/Upgrade.md) +- [Peer dependencies](#peer-dependencies) +- [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) - [Supported elements](#supported-elements) +- [Extensions](#extensions) - [Debug](#debug) - [Example 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` | 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) | `^13.0.1` | 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/docs/bpmn-moddle.md b/docs/bpmn-moddle.md new file mode 100644 index 0000000..75a4354 --- /dev/null +++ b/docs/bpmn-moddle.md @@ -0,0 +1,48 @@ +# 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`'s own source still imports the v9 default export (`import BpmnModdle from 'bpmn-moddle'`). If you let the engine parse XML for you (the `source` option, or `Engine.prototype._getModdleContext`), stay on the 9.x line. To run on 10.x, parse the XML yourself with the named export and pass the result to the engine via the `moddleContext` option: + +```js +import { Engine } from 'bpmn-engine'; +import { BpmnModdle } from 'bpmn-moddle'; // v10 + +const moddleContext = await new BpmnModdle().fromXML(source); +const engine = new Engine({ name: 'on v10', moddleContext }); +``` + +## 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. diff --git a/eslint.config.js b/eslint.config.js index 57280c9..0dd4557 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', { diff --git a/package.json b/package.json index bcfd625..56c0f25 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bpmn-engine", "description": "BPMN 2.0 execution engine. Open source javascript workflow engine.", - "version": "26.0.0", + "version": "26.0.1", "type": "module", "module": "./src/index.js", "main": "./lib/index.cjs", @@ -72,25 +72,28 @@ "@types/bpmn-moddle": "^10.0.0", "@types/node": "^20.19.40", "bent": "^7.3.12", + "bpmn-elements": "^18.0.0", + "bpmn-moddle": "^9.0.4", + "bpmn-moddle-10": "npm:bpmn-moddle@^10.0.0", "c8": "^11.0.0", "camunda-bpmn-moddle": "^7.0.1", "chai": "^6.2.0", "chronokinesis": "^8.0.0", + "debug": "^4.4.3", "eslint": "^10.5.0", "globals": "^17.6.0", + "moddle-context-serializer": "^6.0.0", "mocha": "^11.0.1", "mocha-cakes-2": "^3.3.0", "prettier": "^3.2.5", "rollup": "^4.10.0", "texample": "^1.0.2" }, - "dependencies": { - "bpmn-elements": "^18.0.0", - "bpmn-moddle": "^9.0.4", - "debug": "^4.4.3", - "moddle-context-serializer": "^6.0.0" - }, "peerDependencies": { + "bpmn-elements": ">=18", + "bpmn-moddle": ">=9", + "debug": ">=4", + "moddle-context-serializer": ">=6", "smqp": "^13.0.1" } } diff --git a/test/Engine-test.js b/test/Engine-test.js index bd2e421..92b3b9b 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'; 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..a4ac282 --- /dev/null +++ b/test/feature/bpmn-moddle-backward-compatibility-feature.js @@ -0,0 +1,97 @@ +import { EventEmitter } from 'node:events'; + +import { Engine } from 'bpmn-engine'; +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'); + }); + + let engine, 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; + }); + + let recovered, 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())); + }); + }); +}); diff --git a/test/feature/extend-feature.js b/test/feature/extend-feature.js index 96b25e0..5dc32c9 100644 --- a/test/feature/extend-feature.js +++ b/test/feature/extend-feature.js @@ -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; } } diff --git a/test/feature/issues-feature.js b/test/feature/issues-feature.js index e34cc7e..92fb82a 100644 --- a/test/feature/issues-feature.js +++ b/test/feature/issues-feature.js @@ -1105,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; }