Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
29a5fa6
skip discard flows if not required by process
paed01 Aug 28, 2025
bd9160e
mv feature test
paed01 Sep 9, 2025
3c95907
closing in fixing shake functionality
paed01 Sep 10, 2025
7ea891d
shake link event definition
paed01 Sep 14, 2025
9ee69e0
refactor converging parallel gateway
paed01 Nov 14, 2025
c626bcf
wip
paed01 May 9, 2026
a7249cd
fix linked event definition
paed01 May 9, 2026
b76e2cc
parallel gateway listens for peer enter
paed01 May 9, 2026
01a81a3
revisit link event definition
paed01 May 10, 2026
1082805
prepare for dts-buddy bundling of types
paed01 May 10, 2026
bb76a3f
gather constants
paed01 May 10, 2026
6469ec0
no more default exports
paed01 May 10, 2026
a59f354
build types from implementation
paed01 May 11, 2026
eec7eb9
annotate with types
paed01 May 28, 2026
270fc9c
type annotate activities
paed01 May 30, 2026
87b3032
prototype Message, Escalation and Signal behvaiour
paed01 May 30, 2026
1b5dc81
import from bpmn-elements in tests
paed01 May 31, 2026
e021756
drop sub module bundling files
paed01 May 31, 2026
cb08d5b
definition must be instantiated with new
paed01 May 31, 2026
79f6422
update docs
paed01 May 31, 2026
255b477
handle link events as any inbound triggers
paed01 Jun 3, 2026
5df370f
allow activities to discard all outbound flows
paed01 Jun 6, 2026
cb26c42
cache parallel gateway peers
paed01 Jun 6, 2026
b86fc21
stop discarding sequence flows all together
paed01 Jun 6, 2026
9be635a
multiple start events are mutually exclusive entry points
paed01 Jun 9, 2026
0ab64cb
remove intermediate skipDiscard flag
paed01 Jun 11, 2026
80ec068
remove remnants of discarded flows
paed01 Jun 12, 2026
f0f88bb
enforce mutually exclusive start events on recover
paed01 Jun 13, 2026
44345e4
cleanup some parallel gateway instance props
paed01 Jun 16, 2026
293ceb2
link catch events and start event emit init
paed01 Jun 23, 2026
c617843
intermediage throw event with link is not isEnd
paed01 Jun 24, 2026
9a4ce46
activate and deactivate process extensions
paed01 Jun 27, 2026
9fe19e2
support process run formatting
paed01 Jun 27, 2026
4c79dc1
pass multi instance characteristics as input
paed01 Jun 28, 2026
9ce92d7
fix some return types
paed01 Jul 1, 2026
3c3a4d9
allow extensions with optional activate and deactivate fns
paed01 Jul 1, 2026
0ebdb20
use optional chaining in favor of getPropertyValue
paed01 Jul 2, 2026
261bbd6
tidy up
paed01 Jul 2, 2026
9ad3e16
merge variables.input to keep initial values
paed01 Jul 7, 2026
e321f3f
support parallelMultiple event attribute
paed01 Jul 9, 2026
0a7629b
revisit AdHocSubProcess
paed01 Jul 16, 2026
8fb89e5
type declare Definition run function properly
paed01 Jul 17, 2026
90fddbc
legacy discarded flow does not stall process
paed01 Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ coverage*
*.iml
.vscode

# Agent files (AGENTS.md is the canonical, agent-agnostic doc)
.claude
CLAUDE.md

# Compiled client resources
/public/

Expand All @@ -27,6 +31,9 @@ coverage*
*.swp
*.swo

# TS
*.d.ts.map

# eslint
.eslintcache

Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18
20
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ coverage/
tmp/
test/resources/*.bpmn
*.bpmn
types/index.d.ts
types/index.d.ts.map
87 changes: 87 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# AGENTS.md

This file provides guidance to coding agents (Claude Code, and any tool that reads `AGENTS.md`) when working with code in this repository.

## Workflow

- **TDD is the default.** Red → green → refactor: write or adjust a failing test before changing implementation. Don't delete or weaken existing assertions to land a change — extend them.
- **Performance and coverage are the project's USP.** Avoid regressions in either. On hot paths (broker dispatch, flow traversal, activity activation, joins, multi-instance loops), prefer existing `Context` Maps/refs over rebuilt scans, and avoid per-message allocations/closures where they can be hoisted.
- **JSDoc is concise.** Short intent descriptions are fine; never describe internal implementation.
- Before declaring done: `npm test` (full suite + lint + `dist` rebuild). For coverage-sensitive work, also `npm run cov:html`.

## Commands

- `npm test` — run the full suite in parallel (mocha, `mocha-cakes-2` UI, hot-bev reporter, 3000ms timeout). `posttest` then runs lint and rebuilds `dist/`.
- `npm run lint` — `eslint . --cache && prettier . --check --cache`.
- `npm run dist` — Babel transpile `src/` → `dist/` (also runs on `prepack`).
- `npm run cov:html` — c8 HTML coverage report.
- `npm run test:md` — run texample against code blocks in the documentation markdown files.
- Single test file: `npx mocha test/feature/activity-feature.js`. `.mocharc.json` auto-loads `mocha-cakes-2` and `test/helpers/setup.js` (which registers chai `expect` globally and sets `NODE_ENV=test`).
- Single scenario: add `-g "scenario name"` to the mocha invocation above.
- Note: default mocharc timeout is 1000ms; the `npm test` script bumps it to 3000ms. Long-running scenarios may need `-t 3000` when run standalone.

## Architecture

The library executes BPMN 2.0 workflows. The execution model is message-driven — almost nothing happens by direct method call — so this section focuses on what you cannot learn from any single file.

### Execution hierarchy: Definition → Process → Activity

Each layer pairs a structural wrapper with a dedicated execution orchestrator:

- `src/definition/Definition.js` + `src/definition/DefinitionExecution.js` — top-level, manages executable processes and inter-process messaging.
- `src/process/Process.js` + `src/process/ProcessExecution.js` — owns one `<bpmn:process>`, handles flow traversal, joins, and parallel activation.
- `src/activity/Activity.js` + `src/activity/ActivityExecution.js` — wraps any element (task, event, gateway), tracks postponed/waiting state, drives a per-run behavior instance.

### Message-driven core via `smqp`

All coordination is async message passing on an in-memory AMQP-like broker (`smqp`, a runtime dependency). Each element owns its own `EventBroker` (`src/EventBroker.js`) with exchanges named `event`, `run`, `format`, `execution`, and `api`. Per-element factories wire these up: `DefinitionBroker`, `ProcessBroker`, `ActivityBroker`, `MessageFlowBroker`.

Execution is driven by publishing routing keys like `execute.start`, `execute.completed`, `execute.error`, `run.enter`, `run.end`, `run.discard`, and subscribing via `broker.subscribeTmp()` / `subscribeOnce()`. Messages with `mandatory: true` surface errors if undelivered. The `EventBroker` exposes convenience methods: `on`, `once`, `waitFor`, `emit`, `emitFatal`. If you try to read `ActivityExecution` or `ProcessExecution` as imperative code you will get lost — keep the publish/subscribe model in mind.

**Do not read synchronous queue/exchange state off the broker** (`queue.messageCount`, `consumerCount`, `peek`, etc.). These are `smqp` conveniences a host that swaps in a real AMQP-compliant broker will not provide. Track what you need in execution state instead — e.g. `consumeInbound` keys the consumer assertion off the `initialized` counter, not a pending-message count on `inbound-q`. Publishing, subscribing, asserting/cancelling consumers, and `queueMessage` are all fine; reading queue depth is not.

### Activity vs Behaviour

An element type like `ServiceTask` is not a class. It is a factory function that returns an `Activity` constructed with a `Behaviour` class:

- `Activity` holds structural info: id, type, inbound/outbound flows, broker, lifecycle state.
- `Behaviour` implements the element-specific `execute(executeMessage)` logic, publishing results back through the broker.

When an activity is activated, `ActivityExecution` instantiates the Behaviour and calls its `execute`. To replace an element type entirely, supply a new Behaviour — see `docs/Extend.md`.

To identify an element's kind at runtime, compare its `Behaviour` (`entity.Behaviour === StartEvent`) rather than the `type` string — type strings can be customized via the `types` extension.

### `Context` and `Environment`

- `src/Context.js` is a per-execution **registry and lazy factory**. It stores activities, flows, and processes in `refs` Maps and instantiates them on first access via `upsertActivity` / `upsertSequenceFlow` / `upsertProcess`. It bridges the parsed moddle context (from `bpmn-moddle` via `moddle-context-serializer`) to runtime instances and wires extensions through `ExtensionsMapper`. Contexts are cheap to clone and are isolated per execution scope.
- `src/Environment.js` holds global execution config: `variables`, injected `services`, `timers`, `Scripts` engine, `expressions`, `Logger` factory, and settings such as `batchSize`. Cloned and merged per Definition.

### Api objects

`src/Api.js` produces `ActivityApi` / `ProcessApi` / `DefinitionApi` / `FlowApi`. These are lightweight wrappers over broker messages that event listeners receive (e.g. `definition.on('end', api => …)`). They expose `.signal()`, `.cancel()`, `.fail()`, `.stop()`, `.discard()`, `.resolveExpression()` and serialize running state via `content` and `messageProperties`.

### Extension models

Documented in `docs/Extend.md` and `docs/Extension.md`:

1. **Replace a Behaviour** by passing `{ types: { 'bpmn:StartEvent': MyStartEvent } }` to `Definition`. Use when you need full control over an element's execution.
2. **Non-invasive extension hooks** via `{ extensions: { myExt(activity, context) { … } } }`. Each extension runs once per element after instantiation (every activity **and** the owning process — both call `context.loadExtensions(this)`) and typically attaches listeners or publishes format messages — used for cross-cutting concerns (forms, logging, output capture). An extension may return `{ activate(message), deactivate(message) }` lifecycle hooks; return falsy to only subscribe to events. Gate process-only or activity-only extensions on the element (e.g. `element.type === 'bpmn:Process'`) — the function is called for both.

### State & behavioral invariants

- **No flow discards.** Outbound sequence flows are never discarded; flow and activity `discarded` counters stay `0`. There is no `skipDiscard` setting. Parallel joins rely on cached gateway peers, not on discarded flows.
- **Activities are armed through the inbound queue, then run consumer-driven.** Both start activities (`isStart`, no inbound trigger) and link catch events are armed by `Activity.init()`: it mints an executionId, emits the `init` event (whose placeholder in the process's `postponed` set blocks premature completion), increments an `initialized` counter, and queues a non-persistent `activity.init` message carrying that id (plus the activity `id`) on the activity's own `inbound-q`. The run is then driven by the inbound consumer: the idempotent `consumeInbound()` asserts the consumer when there are inbound triggers, or — even without sequence-flow triggers — when the activity is `initialized` (it reads the `initialized` counter rather than probing `inbound-q` for a pending-message count, so no synchronous smqp-only property is touched), and `_onInbound`'s `activity.init` case decrements the counter and calls `run()` with the carried id. `ProcessExecution._start` arms start activities this way (`init()` then `consumeInbound()`, no direct `run()`). A throwing link publishes `activity.link`; the catch's construction-time inbound-trigger handler calls the catch's own `init()` to arm it identically — there is no `activity.relink`. The `initialized` getter reads the counter (exec-state, not persisted); since the `activity.init` trigger is `persistent: false` it is dropped on recover and never re-consumed, so counter and trigger reset together (a stop/resume while armed cannot desync them).
- **`run()` executionId resolution.** `run(runContent)` uses `runContent.initExecutionId` when `runContent.id` equals the activity's own id, otherwise mints a fresh `getUniqueId(id)`. The init/link handoff (`_onInbound`) always passes a matching `id`, so the reserved id is honoured; every other caller (e.g. delegated event runs passing `run(content.message)`) carries no matching id and gets a fresh one. `run()` does **not** peek the inbound queue, and there is no `initExecutionId` exec-state key. `initExecutionId` is **destructured out** of `runContent` so it never reaches the `run.enter`/`run.start` content (it would otherwise leak through `_createMessage`, which only overwrites `id`/`type`/flags, into every downstream message and saved state).
- **Extension `activate`/`deactivate` fire symmetrically on activities and processes.** Both `Activity` and `Process` call their extensions' `activate(message)` on run enter / redelivered execute / resume and `deactivate(message)` on run leave / stop, each guarded by `if (this.extensions)`. Keep the two in sync: a lifecycle hook added on one side generally belongs on the other. `Definition` does not load or invoke extensions.
- **Run transitions are formatted on both activities and processes.** `Activity._onRunMessage` and `Process._onRunMessage` route every non-passthrough run message through `this.formatter.format(message, cb)` (`MessageFormatter`) before `_continueRunMessage` runs the actual switch; the callback restores status, applies enriched content, or `emitFatal`s on a format error. Formatting is **unconditional** (not gated on `this.extensions`) so any handler — extension or not — can enrich a run by publishing a `format` message, optionally with an `endRoutingKey` to pause the run asynchronously until the matching end key arrives. The run-q message stays unacked across the wait, which is what serializes the run; nothing is `await`ed in the consumer.
- **Multiple start events are mutually exclusive entry points.** The first start event to fire discards the others still armed, so two start branches can never both run.
- **`stateVersion`.** `Definition.getState()` stamps `stateVersion` (the package major, hardcoded in `src/constants.js`); recovering an older major triggers migrations (e.g. start event reconciliation on resume). Unstamped legacy states are treated as version `0`. Bump the constant on each major release.

## Testing patterns

- Framework: mocha + `mocha-cakes-2` BDD UI. `Feature` / `Scenario` / `Given` / `When` / `Then` / `And` / `But` are globals in test files (declared in `eslint.config.js`). Chai `expect` is registered globally via `test/helpers/setup.js`.
- Layout: scenario-style coverage in `test/feature/*.js`; unit tests mirror the `src/` directory tree (`test/activity`, `test/process`, `test/gateways`, `test/tasks`, `test/eventDefinitions`, `test/flows`, …).
- BPMN sources: raw XML templates in `test/helpers/factory.js` (helpers like `factory.valid()`, `factory.userTask()`, `factory.resource('name')`) plus `.bpmn` files under `test/resources/`.
- Primary helper: `test/helpers/testHelpers.js` — `context(source, options)` parses BPMN via `bpmn-moddle`, serializes via `moddle-context-serializer`, and returns a runtime `Context`. Also exposes `Logger`, `emptyContext`, and `AssertMessage` for asserting broker message sequences.
- `test/helpers/JavaScripts.js` is a mock Scripts engine for isolating ScriptTask tests.
- Don't assert on logging — captured `logger.warn`/`debug` output is not part of the tested contract.
87 changes: 87 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,93 @@

## Unreleased

## v18.0.8 - 2026-07-17

### Fixes

- drop legacy in-flight `flow.discard`/`flow.looped` tokens on recover. States saved before the "no flow discards" change could carry a discarded-flow token on the process activity queue; the current runtime never pops it from `postponed`, so recovering such a state stranded process completion
- Type declare Definition run function properly

## v18.0.7 - 2026-07-16

### Additions

- `AdHocSubProcess` is now executed by a dedicated `AdHocSubProcessBehaviour`. Honours `ordering` — Parallel (default) or Sequential, arming one inner start branch at a time — and a `completionCondition` that completes the sub process and cancels the still-running instances, unless `cancelRemainingInstances` is `false`
- catch, boundary, and start events with `parallelMultiple="true"` now wait for **all** their event definitions to fire before completing

## v18.0.6 - 2026-07-06

### Bug fixes

- seeding `environment.variables.input` for a sub process or called process now merges onto any inherited `input` instead of replacing it, so a nested or multi-instance sub process no longer clobbers the parent's `input` namespace (e.g. an inner loop can still resolve `${environment.variables.input.collection}` after the outer loop seeds its context)

## v18.0.5 - 2026-07-01

### Additions

- extension lifecycle hooks `activate` and `deactivate` are now optional; an extension that returns an object without either receives a no-op stub, so returning only one hook — or an object with neither — no longer throws
- the shared `resolve` function on signal-, message-, and escalation reference elements now declares an exported `ResolvedReference` return type

## v18.0.4 - 2026-06-28

### Additions

- called processes and sub processes are seeded with `environment.variables.input` from their input. A multi-instance call activity or sub process passes its loop context (`isSequential`, `index`, `cardinality` and the item under the `elementVariable` name) as `input`

## v18.0.3 - 2026-06-27

### Additions

- process extensions are now activated and deactivated accordingly
- process run messages can be formatted, including asynchronously, the same way as activities

## v18.0.2 - 2026-06-24

- Refactor catching `LinkEventDefinition` trigger and start event init handling. Both publishes `activity.init` to reserve process attention and queues messages on inbound queue that are eventually handled
- a throwing link `IntermediateThrowEvent` is no longer marked as an end (`isEnd`); it has no outbound sequence flows but continues at its catch, so a shake no longer records it as a dead-end sequence
- a converging parallel gateway now publishes `activity.shake.converge` during a shake (previously `activity.shake.join`), matching the runtime `activity.converge` event

## v18.0.1 - 2026-06-13

### Fixes

- enforce mutually exclusive start events on recover: a recovered state where one entry point already won, or a legacy state serialized before the `isStartEvent` flag existed, now correctly discards the start events still left armed instead of resuming them as live entry points

### Additions

- serialized definition state is stamped with a `stateVersion` tracking the package major; recovering an older major (legacy unstamped states are treated as version `0`) triggers migrations such as the start event reconciliation above

## v18.0.0 - 2026-06-11

Refactor parallel converging and forking gateways, and treat multiple start events as mutually exclusive entry points. As a result of the parallel gateway keeping track of peers there is no need for discarding a sequence flows.

### Breaking

- `Definition` must be called with `new`
- parallel gateways now enter execution as soon as the first inbound sequence flow is touched
- removed discarding of outbound sequence flows altogether — activities no longer publish flow discards, so sequence flow and downstream activity `discarded` counters stay at `0`
- IntermediateCatchEvent cannot be used as a starting element, or it can but will not be started by default
- non-gateway activities end the branch when all conditional outbound flows are falsy instead of throwing; only exclusive and inclusive gateways still require a taken or default flow
- multiple start events are mutually exclusive entry points — the first start event to fire discards the others still waiting to be triggered, so two start events can no longer both run (e.g. into a parallel join, or a joining task taken twice)
- start activities that are not start events (e.g. a starting receive task, or an activity without an inbound flow) are no longer auto-discarded; they are genuine tokens that must be signalled or completed
- shake sequence has changed

### Additions

- expose throwable error classes via new `bpmn-elements/errors` subpath: `import { ActivityError, BpmnError, RunError } from 'bpmn-elements/errors'`
- activity readonly property `isParallelJoin` indicating a parallel converging gateway
- activity readonly property `isStartEvent` indicating a start event
- new activity event `activity.converge` published when parallel gateway is executed
- fix link event definition shaking
- fix `Activity.recover()` to return the activity when called without state
- a condition expression resolving to a service function is now invoked with the flow execution scope, supporting sync (return) and async (callback) results
- converging parallel gateways cache their discovered peers per runtime instance, skipping the start-up shake on repeated runs (loops, stop/resume); the cache is rebuilt on recover

### Types

- runtime types are now generated from JSDoc and bundled with [dts-buddy](https://github.com/Rich-Harris/dts-buddy)
- expose `isStartEvent` and `isParallelGateway` on the `Activity` interface

## v17.3.0 - 2025-12-03

- major upgrade of [smqp@11](https://github.com/paed01/smqp/blob/default/CHANGELOG.md)
Expand Down
Loading