Skip to content

Commit 9fe19e2

Browse files
committed
support process run formatting
1 parent 9a4ce46 commit 9fe19e2

8 files changed

Lines changed: 320 additions & 9 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Documented in `docs/Extend.md` and `docs/Extension.md`:
7373
- **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).
7474
- **`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).
7575
- **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.
76+
- **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.
7677
- **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.
7778
- **`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.
7879

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Additions
66

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

910
## v18.0.2 - 2026-06-24
1011

dist/process/Process.js

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ var _ProcessExecution = require("./ProcessExecution.js");
88
var _shared = require("../shared.js");
99
var _Api = require("../Api.js");
1010
var _EventBroker = require("../EventBroker.js");
11+
var _MessageFormatter = require("../MessageFormatter.js");
1112
var _messageHelper = require("../messageHelper.js");
1213
var _Errors = require("../error/Errors.js");
1314
var _constants = require("../constants.js");
1415
const K_LANES = Symbol.for('lanes');
16+
const K_FORMATTER = Symbol.for('formatter');
1517

1618
/**
1719
* Owns one `<bpmn:process>`. Wraps the structural definition and orchestrates flow traversal,
@@ -49,12 +51,14 @@ function Process(processDef, context) {
4951
broker,
5052
on,
5153
once,
52-
waitFor
54+
waitFor,
55+
emitFatal
5356
} = (0, _EventBroker.ProcessBroker)(this);
5457
this.broker = broker;
5558
this.on = on;
5659
this.once = once;
5760
this.waitFor = waitFor;
61+
this.emitFatal = emitFatal;
5862
this[_constants.K_MESSAGE_HANDLERS] = {
5963
onApiMessage: this._onApiMessage.bind(this),
6064
onRunMessage: this._onRunMessage.bind(this),
@@ -84,6 +88,14 @@ Object.defineProperties(Process.prototype, {
8488
return this[_constants.K_EXTENSIONS];
8589
}
8690
},
91+
formatter: {
92+
get() {
93+
let formatter = this[K_FORMATTER];
94+
if (formatter) return formatter;
95+
formatter = this[K_FORMATTER] = new _MessageFormatter.Formatter(this);
96+
return formatter;
97+
}
98+
},
8799
stopped: {
88100
get() {
89101
return this[_constants.K_STOPPED];
@@ -292,14 +304,28 @@ Process.prototype._deactivateRunConsumers = function deactivateRunConsumers() {
292304
};
293305

294306
/** @internal */
295-
Process.prototype._onRunMessage = function onRunMessage(routingKey, message) {
307+
Process.prototype._onRunMessage = function onRunMessage(routingKey, message, messageProperties) {
308+
if (routingKey === 'run.resume') {
309+
return this._onResumeMessage(message);
310+
}
311+
const preStatus = this[_constants.K_STATUS];
312+
this[_constants.K_STATUS] = 'formatting';
313+
return this.formatter.format(message, (err, formattedContent, formatted) => {
314+
this[_constants.K_STATUS] = preStatus;
315+
if (err) {
316+
return this.emitFatal(err, message.content);
317+
}
318+
if (formatted) message.content = formattedContent;
319+
this._continueRunMessage(routingKey, message, messageProperties);
320+
});
321+
};
322+
323+
/** @internal */
324+
Process.prototype._continueRunMessage = function continueRunMessage(routingKey, message) {
296325
const {
297326
content,
298327
fields
299328
} = message;
300-
if (routingKey === 'run.resume') {
301-
return this._onResumeMessage(message);
302-
}
303329
this[_constants.K_STATE_MESSAGE] = message;
304330
switch (routingKey) {
305331
case 'run.enter':

docs/Extension.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ In some cases it may be required to add some extra data when an activity execute
3535

3636
The basic flow is to publish a formatting message on the activity format queue.
3737

38+
The same mechanism works on the **process**: publish a `format` message (optionally with an `endRoutingKey`) in response to a `process.*` event, and the process run pauses at the next transition until the matching end key is published — see the asynchronous example below, which applies equally to process events.
39+
3840
```javascript
3941
import * as elements from 'bpmn-elements';
4042
import { BpmnModdle } from 'bpmn-moddle';

src/process/Process.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ProcessExecution } from './ProcessExecution.js';
22
import { getUniqueId } from '../shared.js';
33
import { ProcessApi } from '../Api.js';
44
import { ProcessBroker } from '../EventBroker.js';
5+
import { Formatter } from '../MessageFormatter.js';
56
import { cloneMessage, cloneContent, cloneParent } from '../messageHelper.js';
67
import { makeErrorFromMessage } from '../error/Errors.js';
78
import {
@@ -17,6 +18,7 @@ import {
1718
} from '../constants.js';
1819

1920
const K_LANES = Symbol.for('lanes');
21+
const K_FORMATTER = Symbol.for('formatter');
2022

2123
/**
2224
* Owns one `<bpmn:process>`. Wraps the structural definition and orchestrates flow traversal,
@@ -47,11 +49,12 @@ export function Process(processDef, context) {
4749
this[K_STATUS] = undefined;
4850
this[K_STOPPED] = false;
4951

50-
const { broker, on, once, waitFor } = ProcessBroker(this);
52+
const { broker, on, once, waitFor, emitFatal } = ProcessBroker(this);
5153
this.broker = broker;
5254
this.on = on;
5355
this.once = once;
5456
this.waitFor = waitFor;
57+
this.emitFatal = emitFatal;
5558

5659
this[K_MESSAGE_HANDLERS] = {
5760
onApiMessage: this._onApiMessage.bind(this),
@@ -83,6 +86,14 @@ Object.defineProperties(Process.prototype, {
8386
return this[K_EXTENSIONS];
8487
},
8588
},
89+
formatter: {
90+
get() {
91+
let formatter = this[K_FORMATTER];
92+
if (formatter) return formatter;
93+
formatter = this[K_FORMATTER] = new Formatter(this);
94+
return formatter;
95+
},
96+
},
8697
stopped: {
8798
get() {
8899
return this[K_STOPPED];
@@ -278,13 +289,28 @@ Process.prototype._deactivateRunConsumers = function deactivateRunConsumers() {
278289
};
279290

280291
/** @internal */
281-
Process.prototype._onRunMessage = function onRunMessage(routingKey, message) {
282-
const { content, fields } = message;
283-
292+
Process.prototype._onRunMessage = function onRunMessage(routingKey, message, messageProperties) {
284293
if (routingKey === 'run.resume') {
285294
return this._onResumeMessage(message);
286295
}
287296

297+
const preStatus = this[K_STATUS];
298+
this[K_STATUS] = 'formatting';
299+
300+
return this.formatter.format(message, (err, formattedContent, formatted) => {
301+
this[K_STATUS] = preStatus;
302+
if (err) {
303+
return this.emitFatal(err, message.content);
304+
}
305+
if (formatted) message.content = formattedContent;
306+
this._continueRunMessage(routingKey, message, messageProperties);
307+
});
308+
};
309+
310+
/** @internal */
311+
Process.prototype._continueRunMessage = function continueRunMessage(routingKey, message) {
312+
const { content, fields } = message;
313+
288314
this[K_STATE_MESSAGE] = message;
289315

290316
switch (routingKey) {

0 commit comments

Comments
 (0)