@nanobpm/bojtos-react 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/BpmnRuntimeView.d.ts +12 -1
- package/dist/BpmnRuntimeView.js +20 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/runState.d.ts +47 -0
- package/dist/runState.js +64 -0
- package/dist/useBojtos.d.ts +71 -4
- package/dist/useBojtos.js +51 -9
- package/package.json +6 -4
- package/src/BpmnRuntimeView.tsx +51 -4
- package/src/index.ts +17 -0
- package/src/runState.ts +72 -0
- package/src/useBojtos.ts +215 -11
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @nanobpm/bojtos-react
|
|
2
2
|
|
|
3
3
|
React binding for the **Bojtos** in-browser BPMN demo framework
|
|
4
|
-
([ADR 0043](../
|
|
4
|
+
([ADR 0043](../README.md#design)), built on
|
|
5
5
|
[`@nanobpm/bojtos-kit`](../bojtos-kit).
|
|
6
6
|
|
|
7
7
|
- **`useBojtos({ bpmn })`** — owns the engine session and the reactive
|
|
@@ -51,6 +51,7 @@ consumer must import bpmn-js's diagram CSS once and provide the `.nano-active` /
|
|
|
51
51
|
## Build
|
|
52
52
|
|
|
53
53
|
`dist/` (the tsc-emitted JS + `.d.ts`, with JSX already compiled to
|
|
54
|
-
`react/jsx-runtime` so consumers never re-transform node_modules) is
|
|
55
|
-
|
|
54
|
+
`react/jsx-runtime` so consumers never re-transform node_modules) is what ships,
|
|
55
|
+
built by `prepack` on publish. It is **not** committed — `.gitignore` covers it —
|
|
56
|
+
so build before pointing a `file:` consumer at this workspace. Regenerate with
|
|
56
57
|
`npm run build`.
|
|
@@ -7,6 +7,17 @@ export interface BpmnRuntimeViewProps {
|
|
|
7
7
|
incidentIds: string[];
|
|
8
8
|
/** Optional class for the container element (it always fills its parent). */
|
|
9
9
|
className?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Accessible name for the diagram. The token and incident highlights are
|
|
12
|
+
* purely visual, so without this a screen-reader user is told nothing at all
|
|
13
|
+
* about what is running.
|
|
14
|
+
*/
|
|
15
|
+
label?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Map an element id to a human name for the live status announcement — pass
|
|
18
|
+
* the diagram's element names if you have them. Defaults to the raw id.
|
|
19
|
+
*/
|
|
20
|
+
elementName?: (elementId: string) => string;
|
|
10
21
|
}
|
|
11
22
|
/**
|
|
12
23
|
* Read-only diagram that imports the XML once and updates token/incident markers
|
|
@@ -20,4 +31,4 @@ export interface BpmnRuntimeViewProps {
|
|
|
20
31
|
* and provide the `.nano-active` / `.nano-incident` marker styles plus a
|
|
21
32
|
* `.nano-token` style for the token badge overlaid on each active element.
|
|
22
33
|
*/
|
|
23
|
-
export declare function BpmnRuntimeView({ xml, activeIds, incidentIds, className, }: BpmnRuntimeViewProps): import("react").JSX.Element;
|
|
34
|
+
export declare function BpmnRuntimeView({ xml, activeIds, incidentIds, className, label, elementName, }: BpmnRuntimeViewProps): import("react").JSX.Element;
|
package/dist/BpmnRuntimeView.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import NavigatedViewer from "bpmn-js/lib/NavigatedViewer";
|
|
4
|
+
import { describeRunState, markerKey } from "./runState.js";
|
|
4
5
|
/**
|
|
5
6
|
* Read-only diagram that imports the XML once and updates token/incident markers
|
|
6
7
|
* in place (no re-import, so the zoom/scroll position is preserved while
|
|
@@ -13,7 +14,7 @@ import NavigatedViewer from "bpmn-js/lib/NavigatedViewer";
|
|
|
13
14
|
* and provide the `.nano-active` / `.nano-incident` marker styles plus a
|
|
14
15
|
* `.nano-token` style for the token badge overlaid on each active element.
|
|
15
16
|
*/
|
|
16
|
-
export function BpmnRuntimeView({ xml, activeIds, incidentIds, className, }) {
|
|
17
|
+
export function BpmnRuntimeView({ xml, activeIds, incidentIds, className, label = "BPMN process diagram", elementName, }) {
|
|
17
18
|
const containerRef = useRef(null);
|
|
18
19
|
const viewerRef = useRef(null);
|
|
19
20
|
const importedRef = useRef(false);
|
|
@@ -101,9 +102,24 @@ export function BpmnRuntimeView({ xml, activeIds, incidentIds, className, }) {
|
|
|
101
102
|
}
|
|
102
103
|
tokenOverlaysRef.current = nextOverlays;
|
|
103
104
|
}
|
|
105
|
+
// `activeIds` / `incidentIds` are almost always fresh arrays (`snapshot?.x ??
|
|
106
|
+
// []`), so depending on their identity re-painted every marker and re-created
|
|
107
|
+
// every token overlay on every render of the parent — visible churn on a busy
|
|
108
|
+
// diagram. Depend on the ids themselves instead.
|
|
109
|
+
const key = markerKey(activeIds, incidentIds);
|
|
104
110
|
useEffect(() => {
|
|
105
111
|
applyMarkers();
|
|
106
112
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
|
-
}, [
|
|
108
|
-
return (_jsx("div", { ref: containerRef,
|
|
113
|
+
}, [key]);
|
|
114
|
+
return (_jsxs("div", { className: className, style: { width: "100%", height: "100%", position: "relative" }, children: [_jsx("div", { ref: containerRef, role: "img", "aria-label": label, style: { width: "100%", height: "100%" } }), _jsx("div", { role: "status", "aria-live": "polite", style: {
|
|
115
|
+
position: "absolute",
|
|
116
|
+
width: 1,
|
|
117
|
+
height: 1,
|
|
118
|
+
margin: -1,
|
|
119
|
+
padding: 0,
|
|
120
|
+
overflow: "hidden",
|
|
121
|
+
clip: "rect(0 0 0 0)",
|
|
122
|
+
whiteSpace: "nowrap",
|
|
123
|
+
border: 0,
|
|
124
|
+
}, children: describeRunState(activeIds, incidentIds, elementName) })] }));
|
|
109
125
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { useBojtos, type UseBojtosOptions, type BojtosControls, type BojtosPhase
|
|
|
2
2
|
export { Bojtos, type BojtosProps, type TraceEvent } from "./Bojtos.js";
|
|
3
3
|
export { OrderFulfillmentDemo, ORDER_FULFILLMENT_BPMN, orderFulfillmentWorkers, } from "./examples/orderFulfillment.js";
|
|
4
4
|
export { BpmnRuntimeView, type BpmnRuntimeViewProps, } from "./BpmnRuntimeView.js";
|
|
5
|
-
export {
|
|
6
|
-
export
|
|
5
|
+
export { describeRunState, markerKey, bpmnKey, resourceList, capEvents, } from "./runState.js";
|
|
6
|
+
export { JobFailure, settleReason, unhandledJobTypes, type JobHandler, type JobResult, type AgentHandler, type DispatchOptions, type DispatchResult, type RoundResult, type SettleReason, } from "@nanobpm/bojtos-kit";
|
|
7
|
+
export type { BojtosSession, Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, UserTaskDto, MessageSubscriptionDto, SignalSubscriptionDto, ElementStatDto, SequenceFlowDto, DecisionInstanceDto, ActiveEl, ActivateInstruction, AgentActivation, AgentResult, WasmEvent, } from "@nanobpm/bojtos-kit";
|
package/dist/index.js
CHANGED
|
@@ -7,4 +7,5 @@ export { useBojtos, } from "./useBojtos.js";
|
|
|
7
7
|
export { Bojtos } from "./Bojtos.js";
|
|
8
8
|
export { OrderFulfillmentDemo, ORDER_FULFILLMENT_BPMN, orderFulfillmentWorkers, } from "./examples/orderFulfillment.js";
|
|
9
9
|
export { BpmnRuntimeView, } from "./BpmnRuntimeView.js";
|
|
10
|
-
export {
|
|
10
|
+
export { describeRunState, markerKey, bpmnKey, resourceList, capEvents, } from "./runState.js";
|
|
11
|
+
export { JobFailure, settleReason, unhandledJobTypes, } from "@nanobpm/bojtos-kit";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers behind the React binding's decisions.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the component and the hook — and out of any module that imports
|
|
5
|
+
* bpmn-js — so they can be tested without a DOM or the peer dependency. The
|
|
6
|
+
* component and hook hold rendering and lifecycle; what counts as a change, how
|
|
7
|
+
* a run reads aloud, which resources to deploy and how far to trim the log are
|
|
8
|
+
* decisions, and decisions are worth testing.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Stable key for a marker set, so unchanged ids don't re-paint the diagram.
|
|
12
|
+
*
|
|
13
|
+
* Relies on BPMN element ids being XML NCNames (so they can't contain `,` or
|
|
14
|
+
* `|`) — that invariant is what keeps the two-field join collision-free. If ids
|
|
15
|
+
* could contain the delimiters, `["a,b"],[]` and `["a"],["b"]`-style pairs
|
|
16
|
+
* would key alike.
|
|
17
|
+
*/
|
|
18
|
+
export declare function markerKey(activeIds: string[], incidentIds: string[]): string;
|
|
19
|
+
/**
|
|
20
|
+
* Content key for the `bpmn` prop, so a fresh array identity each render doesn't
|
|
21
|
+
* re-create the engine but a real content change still does. Only the array
|
|
22
|
+
* case is serialized (with a boundary-preserving `JSON.stringify`, not a
|
|
23
|
+
* `join` — a delimiter join lets two different resource arrays collapse to one
|
|
24
|
+
* key when a resource borders/contains the delimiter, silently missing a real
|
|
25
|
+
* change). A lone string can't have array-boundary collisions and React deps
|
|
26
|
+
* already compare strings by value, so it passes through untouched — no needless
|
|
27
|
+
* re-walk of potentially large BPMN XML each render.
|
|
28
|
+
*/
|
|
29
|
+
export declare function bpmnKey(bpmn: string | string[]): string;
|
|
30
|
+
/**
|
|
31
|
+
* Normalize the `bpmn` prop to an ordered resource list for deployment. Deploy
|
|
32
|
+
* order is significant: a later resource can reference an earlier one (a call
|
|
33
|
+
* activity's child), so the array order is preserved verbatim.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resourceList(bpmn: string | string[]): string[];
|
|
36
|
+
/**
|
|
37
|
+
* Trim an event log to the consumer's cap, keeping the most recent events.
|
|
38
|
+
* `undefined` or a negative cap means "no cap"; `0` means "keep nothing". The
|
|
39
|
+
* input array is never mutated.
|
|
40
|
+
*/
|
|
41
|
+
export declare function capEvents<T>(all: T[], cap: number | undefined): T[];
|
|
42
|
+
/**
|
|
43
|
+
* A one-line description of run state, for the diagram's live region. The token
|
|
44
|
+
* and incident highlights are purely visual; this is the same information as
|
|
45
|
+
* text.
|
|
46
|
+
*/
|
|
47
|
+
export declare function describeRunState(activeIds: string[], incidentIds: string[], name?: (id: string) => string): string;
|
package/dist/runState.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers behind the React binding's decisions.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the component and the hook — and out of any module that imports
|
|
5
|
+
* bpmn-js — so they can be tested without a DOM or the peer dependency. The
|
|
6
|
+
* component and hook hold rendering and lifecycle; what counts as a change, how
|
|
7
|
+
* a run reads aloud, which resources to deploy and how far to trim the log are
|
|
8
|
+
* decisions, and decisions are worth testing.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Stable key for a marker set, so unchanged ids don't re-paint the diagram.
|
|
12
|
+
*
|
|
13
|
+
* Relies on BPMN element ids being XML NCNames (so they can't contain `,` or
|
|
14
|
+
* `|`) — that invariant is what keeps the two-field join collision-free. If ids
|
|
15
|
+
* could contain the delimiters, `["a,b"],[]` and `["a"],["b"]`-style pairs
|
|
16
|
+
* would key alike.
|
|
17
|
+
*/
|
|
18
|
+
export function markerKey(activeIds, incidentIds) {
|
|
19
|
+
return `${activeIds.join(",")}|${incidentIds.join(",")}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Content key for the `bpmn` prop, so a fresh array identity each render doesn't
|
|
23
|
+
* re-create the engine but a real content change still does. Only the array
|
|
24
|
+
* case is serialized (with a boundary-preserving `JSON.stringify`, not a
|
|
25
|
+
* `join` — a delimiter join lets two different resource arrays collapse to one
|
|
26
|
+
* key when a resource borders/contains the delimiter, silently missing a real
|
|
27
|
+
* change). A lone string can't have array-boundary collisions and React deps
|
|
28
|
+
* already compare strings by value, so it passes through untouched — no needless
|
|
29
|
+
* re-walk of potentially large BPMN XML each render.
|
|
30
|
+
*/
|
|
31
|
+
export function bpmnKey(bpmn) {
|
|
32
|
+
return Array.isArray(bpmn) ? JSON.stringify(bpmn) : bpmn;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Normalize the `bpmn` prop to an ordered resource list for deployment. Deploy
|
|
36
|
+
* order is significant: a later resource can reference an earlier one (a call
|
|
37
|
+
* activity's child), so the array order is preserved verbatim.
|
|
38
|
+
*/
|
|
39
|
+
export function resourceList(bpmn) {
|
|
40
|
+
return Array.isArray(bpmn) ? bpmn : [bpmn];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Trim an event log to the consumer's cap, keeping the most recent events.
|
|
44
|
+
* `undefined` or a negative cap means "no cap"; `0` means "keep nothing". The
|
|
45
|
+
* input array is never mutated.
|
|
46
|
+
*/
|
|
47
|
+
export function capEvents(all, cap) {
|
|
48
|
+
return cap !== undefined && cap >= 0 && all.length > cap
|
|
49
|
+
? all.slice(all.length - cap)
|
|
50
|
+
: all;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A one-line description of run state, for the diagram's live region. The token
|
|
54
|
+
* and incident highlights are purely visual; this is the same information as
|
|
55
|
+
* text.
|
|
56
|
+
*/
|
|
57
|
+
export function describeRunState(activeIds, incidentIds, name = (id) => id) {
|
|
58
|
+
const parts = [];
|
|
59
|
+
if (activeIds.length)
|
|
60
|
+
parts.push(`Running: ${activeIds.map(name).join(", ")}`);
|
|
61
|
+
if (incidentIds.length)
|
|
62
|
+
parts.push(`Incident: ${incidentIds.map(name).join(", ")}`);
|
|
63
|
+
return parts.length ? parts.join(". ") : "Nothing running";
|
|
64
|
+
}
|
package/dist/useBojtos.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { type AgentResult, type DispatchOptions, type JobHandler, type RoundResult, type Snapshot, type WasmEvent, type WasmSource } from "@nanobpm/bojtos-kit";
|
|
1
|
+
import { type ActivateInstruction, type AgentResult, type DispatchOptions, type JobHandler, type RoundResult, type Snapshot, type WasmEvent, type WasmSource } from "@nanobpm/bojtos-kit";
|
|
2
2
|
/** Lifecycle of the in-browser engine load. */
|
|
3
3
|
export type BojtosPhase = "loading" | "ready" | "error";
|
|
4
4
|
export interface UseBojtosOptions {
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The BPMN to deploy. Re-deploys on a fresh engine when it changes.
|
|
7
|
+
*
|
|
8
|
+
* Pass an array to deploy several resources into one engine — a called
|
|
9
|
+
* process alongside its parent, say. `processIds` then lists every deployable
|
|
10
|
+
* process across all of them, in deployment order.
|
|
11
|
+
*/
|
|
12
|
+
bpmn: string | string[];
|
|
7
13
|
/**
|
|
8
14
|
* Optional engine wasm source. Pass a `URL` / bytes / `WebAssembly.Module`
|
|
9
15
|
* when the default `import.meta.url` loader can't resolve the binary (the
|
|
@@ -14,6 +20,15 @@ export interface UseBojtosOptions {
|
|
|
14
20
|
* reload the module.
|
|
15
21
|
*/
|
|
16
22
|
wasm?: WasmSource;
|
|
23
|
+
/**
|
|
24
|
+
* Cap the reactive `events` log at the most recent N entries.
|
|
25
|
+
*
|
|
26
|
+
* Every command re-reads the engine's full event log into React state, so a
|
|
27
|
+
* long-running demo copies an ever-growing array on each step. Set this when
|
|
28
|
+
* a page runs for a while and only shows a tail; leave it unset to keep the
|
|
29
|
+
* whole log, which stays the default so existing consumers are unaffected.
|
|
30
|
+
*/
|
|
31
|
+
maxEvents?: number;
|
|
17
32
|
}
|
|
18
33
|
export interface BojtosControls {
|
|
19
34
|
phase: BojtosPhase;
|
|
@@ -46,6 +61,58 @@ export interface BojtosControls {
|
|
|
46
61
|
correlateMessage(messageName: string, correlationKey: string, variablesJson: string): Snapshot | null;
|
|
47
62
|
/** Advance the virtual clock. */
|
|
48
63
|
advanceTime(byMs: number): Snapshot | null;
|
|
64
|
+
/**
|
|
65
|
+
* Throw a BPMN business error from a waiting job: interrupts the activity via
|
|
66
|
+
* a matching error boundary/event-subprocess catch, or raises an incident if
|
|
67
|
+
* uncaught. The job is consumed either way.
|
|
68
|
+
*/
|
|
69
|
+
throwError(jobKey: string, errorCode: string, errorMessage: string): Snapshot | null;
|
|
70
|
+
/**
|
|
71
|
+
* Set a job's remaining retries. Used to recover a job parked on a no-retries
|
|
72
|
+
* incident before resolving that incident; does not itself unblock the job.
|
|
73
|
+
*/
|
|
74
|
+
updateRetries(jobKey: string, retries: number): Snapshot | null;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve an open incident by key, retrying the work that failed. Pair with
|
|
77
|
+
* {@link updateRetries} to make a failed job activatable again — the
|
|
78
|
+
* incident/retry loop a demo needs to show recovery.
|
|
79
|
+
*/
|
|
80
|
+
resolveIncident(incidentKey: string): Snapshot | null;
|
|
81
|
+
/**
|
|
82
|
+
* Merge variables into a scope (a process-instance or element-instance key).
|
|
83
|
+
* With `local`, they are written strictly into that scope; otherwise they
|
|
84
|
+
* propagate up to the nearest ancestor defining each name.
|
|
85
|
+
*/
|
|
86
|
+
setVariables(scopeKey: string, variablesJson: string, local: boolean): Snapshot | null;
|
|
87
|
+
/** Broadcast a signal by name to every matching open subscription. */
|
|
88
|
+
broadcastSignal(signalName: string, variablesJson: string): Snapshot | null;
|
|
89
|
+
/** Cancel (terminate) a running process instance. */
|
|
90
|
+
cancelInstance(instanceKey: string): Snapshot | null;
|
|
91
|
+
/**
|
|
92
|
+
* Modify a running instance: terminate element instances and/or activate new
|
|
93
|
+
* ones (Zeebe "modify process instance").
|
|
94
|
+
*/
|
|
95
|
+
modify(instanceKey: string, activateInstructions: ActivateInstruction[], terminateElementInstanceKeys: string[]): Snapshot | null;
|
|
96
|
+
/**
|
|
97
|
+
* Complete a waiting user task, merging output variables.
|
|
98
|
+
*
|
|
99
|
+
* A `userTask` produces no job, so the dispatch loop cannot advance one: this
|
|
100
|
+
* is the only way a model with a human step reaches its end event. Drive it
|
|
101
|
+
* from `snapshot.userTasks`.
|
|
102
|
+
*/
|
|
103
|
+
completeUserTask(userTaskKey: string, variablesJson: string): Snapshot | null;
|
|
104
|
+
/**
|
|
105
|
+
* Assign a user task. With `allowOverride` false the command is rejected if
|
|
106
|
+
* the task already has an assignee.
|
|
107
|
+
*/
|
|
108
|
+
assignUserTask(userTaskKey: string, assignee: string, allowOverride: boolean): Snapshot | null;
|
|
109
|
+
/** Clear a user task's assignee. */
|
|
110
|
+
unassignUserTask(userTaskKey: string): Snapshot | null;
|
|
111
|
+
/**
|
|
112
|
+
* Update a user task's attributes from a JSON changeset (`candidateGroups`,
|
|
113
|
+
* `candidateUsers`, `dueDate`, `followUpDate`, `priority`).
|
|
114
|
+
*/
|
|
115
|
+
updateUserTask(userTaskKey: string, changesetJson: string): Snapshot | null;
|
|
49
116
|
/**
|
|
50
117
|
* Run the registered worker handlers until the process settles (activate →
|
|
51
118
|
* handler → complete/fail), then reflect the resulting snapshot/events.
|
|
@@ -76,4 +143,4 @@ export interface BojtosControls {
|
|
|
76
143
|
* test-run panel is its first consumer (§8 step 2 — dogfooding is the acceptance
|
|
77
144
|
* test).
|
|
78
145
|
*/
|
|
79
|
-
export declare function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls;
|
|
146
|
+
export declare function useBojtos({ bpmn, wasm, maxEvents, }: UseBojtosOptions): BojtosControls;
|
package/dist/useBojtos.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { createBojtosSession, dispatchRound, dispatchWorkers, } from "@nanobpm/bojtos-kit";
|
|
3
|
+
import { bpmnKey, capEvents, resourceList } from "./runState.js";
|
|
3
4
|
/**
|
|
4
5
|
* React binding over a headless {@link BojtosSession}: owns the engine's
|
|
5
6
|
* lifecycle and the reactive `snapshot` / `events` / `processIds` state, and
|
|
@@ -11,7 +12,7 @@ import { createBojtosSession, dispatchRound, dispatchWorkers, } from "@nanobpm/b
|
|
|
11
12
|
* test-run panel is its first consumer (§8 step 2 — dogfooding is the acceptance
|
|
12
13
|
* test).
|
|
13
14
|
*/
|
|
14
|
-
export function useBojtos({ bpmn, wasm }) {
|
|
15
|
+
export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
15
16
|
const sessionRef = useRef(null);
|
|
16
17
|
const [phase, setPhase] = useState("loading");
|
|
17
18
|
const [error, setError] = useState(null);
|
|
@@ -23,13 +24,32 @@ export function useBojtos({ bpmn, wasm }) {
|
|
|
23
24
|
// identity each render must not re-create the session.
|
|
24
25
|
const wasmRef = useRef(wasm);
|
|
25
26
|
wasmRef.current = wasm;
|
|
27
|
+
// An array prop has a fresh identity every render, which would re-create the
|
|
28
|
+
// engine on each one. Key the deploy effect on the content instead — see
|
|
29
|
+
// `bpmnKey` for why this is a boundary-preserving serialization, not a join.
|
|
30
|
+
const deployKey = bpmnKey(bpmn);
|
|
31
|
+
const bpmnRef = useRef(bpmn);
|
|
32
|
+
bpmnRef.current = bpmn;
|
|
33
|
+
// Trim the reactive event log when the consumer asked for a cap.
|
|
34
|
+
const maxEventsRef = useRef(maxEvents);
|
|
35
|
+
maxEventsRef.current = maxEvents;
|
|
36
|
+
const readEvents = useCallback((session) => {
|
|
37
|
+
return capEvents(session.events(), maxEventsRef.current);
|
|
38
|
+
}, []);
|
|
26
39
|
const deployInto = useCallback((session) => {
|
|
27
|
-
const
|
|
28
|
-
|
|
40
|
+
const resources = resourceList(bpmnRef.current);
|
|
41
|
+
// Deploy in order, collecting every deployable process id. A later
|
|
42
|
+
// resource can reference an earlier one (a call activity's child).
|
|
43
|
+
const ids = [];
|
|
44
|
+
for (const xml of resources)
|
|
45
|
+
ids.push(...session.deploy(xml).processIds);
|
|
46
|
+
setProcessIds(ids);
|
|
29
47
|
setSnapshot(null);
|
|
30
48
|
setEvents([]);
|
|
31
49
|
setError(null);
|
|
32
|
-
},
|
|
50
|
+
},
|
|
51
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
|
+
[deployKey]);
|
|
33
53
|
useEffect(() => {
|
|
34
54
|
let cancelled = false;
|
|
35
55
|
// A new diagram means a fresh engine: drop back to `loading` and clear the
|
|
@@ -81,7 +101,7 @@ export function useBojtos({ bpmn, wasm }) {
|
|
|
81
101
|
try {
|
|
82
102
|
const snap = fn(session);
|
|
83
103
|
setSnapshot(snap);
|
|
84
|
-
setEvents(session
|
|
104
|
+
setEvents(readEvents(session));
|
|
85
105
|
setError(null);
|
|
86
106
|
return snap;
|
|
87
107
|
}
|
|
@@ -96,6 +116,17 @@ export function useBojtos({ bpmn, wasm }) {
|
|
|
96
116
|
const failJob = useCallback((jobKey, retries, message) => run((s) => s.failJob(jobKey, retries, message)), [run]);
|
|
97
117
|
const advanceTime = useCallback((byMs) => run((s) => s.advanceTime(byMs)), [run]);
|
|
98
118
|
const correlateMessage = useCallback((messageName, correlationKey, variablesJson) => run((s) => s.correlateMessage(messageName, correlationKey, variablesJson)), [run]);
|
|
119
|
+
const throwError = useCallback((jobKey, errorCode, errorMessage) => run((s) => s.throwError(jobKey, errorCode, errorMessage)), [run]);
|
|
120
|
+
const updateRetries = useCallback((jobKey, retries) => run((s) => s.updateRetries(jobKey, retries)), [run]);
|
|
121
|
+
const resolveIncident = useCallback((incidentKey) => run((s) => s.resolveIncident(incidentKey)), [run]);
|
|
122
|
+
const setVariables = useCallback((scopeKey, variablesJson, local) => run((s) => s.setVariables(scopeKey, variablesJson, local)), [run]);
|
|
123
|
+
const broadcastSignal = useCallback((signalName, variablesJson) => run((s) => s.broadcastSignal(signalName, variablesJson)), [run]);
|
|
124
|
+
const cancelInstance = useCallback((instanceKey) => run((s) => s.cancelInstance(instanceKey)), [run]);
|
|
125
|
+
const modify = useCallback((instanceKey, activateInstructions, terminateElementInstanceKeys) => run((s) => s.modify(instanceKey, activateInstructions, terminateElementInstanceKeys)), [run]);
|
|
126
|
+
const completeUserTask = useCallback((userTaskKey, variablesJson) => run((s) => s.completeUserTask(userTaskKey, variablesJson)), [run]);
|
|
127
|
+
const assignUserTask = useCallback((userTaskKey, assignee, allowOverride) => run((s) => s.assignUserTask(userTaskKey, assignee, allowOverride)), [run]);
|
|
128
|
+
const unassignUserTask = useCallback((userTaskKey) => run((s) => s.unassignUserTask(userTaskKey)), [run]);
|
|
129
|
+
const updateUserTask = useCallback((userTaskKey, changesetJson) => run((s) => s.updateUserTask(userTaskKey, changesetJson)), [run]);
|
|
99
130
|
const runWorkers = useCallback(async (workers, opts) => {
|
|
100
131
|
const session = sessionRef.current;
|
|
101
132
|
if (!session)
|
|
@@ -107,7 +138,7 @@ export function useBojtos({ bpmn, wasm }) {
|
|
|
107
138
|
if (sessionRef.current !== session)
|
|
108
139
|
return null;
|
|
109
140
|
setSnapshot(settled);
|
|
110
|
-
setEvents(session
|
|
141
|
+
setEvents(readEvents(session));
|
|
111
142
|
setError(null);
|
|
112
143
|
return settled;
|
|
113
144
|
}
|
|
@@ -117,7 +148,7 @@ export function useBojtos({ bpmn, wasm }) {
|
|
|
117
148
|
// Reflect whatever state the engine reached before the drain aborted
|
|
118
149
|
// (e.g. the maxRounds guard) so the view isn't left stale.
|
|
119
150
|
setSnapshot(session.snapshot());
|
|
120
|
-
setEvents(session
|
|
151
|
+
setEvents(readEvents(session));
|
|
121
152
|
setError(String(e));
|
|
122
153
|
return null;
|
|
123
154
|
}
|
|
@@ -132,7 +163,7 @@ export function useBojtos({ bpmn, wasm }) {
|
|
|
132
163
|
if (sessionRef.current !== session)
|
|
133
164
|
return null;
|
|
134
165
|
setSnapshot(round.snapshot);
|
|
135
|
-
setEvents(session
|
|
166
|
+
setEvents(readEvents(session));
|
|
136
167
|
setError(null);
|
|
137
168
|
return round;
|
|
138
169
|
}
|
|
@@ -140,7 +171,7 @@ export function useBojtos({ bpmn, wasm }) {
|
|
|
140
171
|
if (sessionRef.current !== session)
|
|
141
172
|
return null;
|
|
142
173
|
setSnapshot(session.snapshot());
|
|
143
|
-
setEvents(session
|
|
174
|
+
setEvents(readEvents(session));
|
|
144
175
|
setError(String(e));
|
|
145
176
|
return null;
|
|
146
177
|
}
|
|
@@ -172,6 +203,17 @@ export function useBojtos({ bpmn, wasm }) {
|
|
|
172
203
|
failJob,
|
|
173
204
|
advanceTime,
|
|
174
205
|
correlateMessage,
|
|
206
|
+
throwError,
|
|
207
|
+
updateRetries,
|
|
208
|
+
resolveIncident,
|
|
209
|
+
setVariables,
|
|
210
|
+
broadcastSignal,
|
|
211
|
+
cancelInstance,
|
|
212
|
+
modify,
|
|
213
|
+
completeUserTask,
|
|
214
|
+
assignUserTask,
|
|
215
|
+
unassignUserTask,
|
|
216
|
+
updateUserTask,
|
|
175
217
|
runWorkers,
|
|
176
218
|
stepWorkers,
|
|
177
219
|
reset,
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/bojtos-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "React binding for the Bojtos in-browser BPMN demo framework (ADR 0043): the useBojtos hook (owns the engine session + reactive snapshot/event state) and the <BpmnRuntimeView> live token/incident diagram. Built on @nanobpm/bojtos-kit; the console test-run panel is its first consumer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/
|
|
9
|
+
"url": "git+https://github.com/nanobpm/bojtos.git",
|
|
10
10
|
"directory": "bojtos-react"
|
|
11
11
|
},
|
|
12
12
|
"main": "./dist/index.js",
|
|
@@ -27,10 +27,12 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsc -p tsconfig.json",
|
|
29
29
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
30
|
-
"prepack": "npm run build"
|
|
30
|
+
"prepack": "npm run build",
|
|
31
|
+
"test": "npm run build && npm run test:ci",
|
|
32
|
+
"test:ci": "node --experimental-strip-types --test test/*.test.ts"
|
|
31
33
|
},
|
|
32
34
|
"dependencies": {
|
|
33
|
-
"@nanobpm/bojtos-kit": "^0.
|
|
35
|
+
"@nanobpm/bojtos-kit": "^0.5.0"
|
|
34
36
|
},
|
|
35
37
|
"peerDependencies": {
|
|
36
38
|
"bpmn-js": ">=17",
|
package/src/BpmnRuntimeView.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
import NavigatedViewer from "bpmn-js/lib/NavigatedViewer";
|
|
3
|
+
import { describeRunState, markerKey } from "./runState.js";
|
|
3
4
|
|
|
4
5
|
interface Canvas {
|
|
5
6
|
zoom(mode: string): void;
|
|
@@ -27,6 +28,17 @@ export interface BpmnRuntimeViewProps {
|
|
|
27
28
|
incidentIds: string[];
|
|
28
29
|
/** Optional class for the container element (it always fills its parent). */
|
|
29
30
|
className?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Accessible name for the diagram. The token and incident highlights are
|
|
33
|
+
* purely visual, so without this a screen-reader user is told nothing at all
|
|
34
|
+
* about what is running.
|
|
35
|
+
*/
|
|
36
|
+
label?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Map an element id to a human name for the live status announcement — pass
|
|
39
|
+
* the diagram's element names if you have them. Defaults to the raw id.
|
|
40
|
+
*/
|
|
41
|
+
elementName?: (elementId: string) => string;
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
/**
|
|
@@ -46,6 +58,8 @@ export function BpmnRuntimeView({
|
|
|
46
58
|
activeIds,
|
|
47
59
|
incidentIds,
|
|
48
60
|
className,
|
|
61
|
+
label = "BPMN process diagram",
|
|
62
|
+
elementName,
|
|
49
63
|
}: BpmnRuntimeViewProps) {
|
|
50
64
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
51
65
|
const viewerRef = useRef<NavigatedViewer | null>(null);
|
|
@@ -133,16 +147,49 @@ export function BpmnRuntimeView({
|
|
|
133
147
|
tokenOverlaysRef.current = nextOverlays;
|
|
134
148
|
}
|
|
135
149
|
|
|
150
|
+
// `activeIds` / `incidentIds` are almost always fresh arrays (`snapshot?.x ??
|
|
151
|
+
// []`), so depending on their identity re-painted every marker and re-created
|
|
152
|
+
// every token overlay on every render of the parent — visible churn on a busy
|
|
153
|
+
// diagram. Depend on the ids themselves instead.
|
|
154
|
+
const key = markerKey(activeIds, incidentIds);
|
|
136
155
|
useEffect(() => {
|
|
137
156
|
applyMarkers();
|
|
138
157
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
139
|
-
}, [
|
|
158
|
+
}, [key]);
|
|
140
159
|
|
|
141
160
|
return (
|
|
142
161
|
<div
|
|
143
|
-
ref={containerRef}
|
|
144
162
|
className={className}
|
|
145
|
-
style={{ width: "100%", height: "100%" }}
|
|
146
|
-
|
|
163
|
+
style={{ width: "100%", height: "100%", position: "relative" }}
|
|
164
|
+
>
|
|
165
|
+
<div
|
|
166
|
+
ref={containerRef}
|
|
167
|
+
role="img"
|
|
168
|
+
aria-label={label}
|
|
169
|
+
style={{ width: "100%", height: "100%" }}
|
|
170
|
+
/>
|
|
171
|
+
{/* The highlights are visual only. Mirror them as text, politely
|
|
172
|
+
announced, so the run is followable without seeing the diagram. No
|
|
173
|
+
`aria-label` here: on a live region it would override the changing
|
|
174
|
+
text in the accessible-name computation, so screen readers would
|
|
175
|
+
announce the static label instead of the run state. */}
|
|
176
|
+
<div
|
|
177
|
+
role="status"
|
|
178
|
+
aria-live="polite"
|
|
179
|
+
style={{
|
|
180
|
+
position: "absolute",
|
|
181
|
+
width: 1,
|
|
182
|
+
height: 1,
|
|
183
|
+
margin: -1,
|
|
184
|
+
padding: 0,
|
|
185
|
+
overflow: "hidden",
|
|
186
|
+
clip: "rect(0 0 0 0)",
|
|
187
|
+
whiteSpace: "nowrap",
|
|
188
|
+
border: 0,
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{describeRunState(activeIds, incidentIds, elementName)}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
147
194
|
);
|
|
148
195
|
}
|
package/src/index.ts
CHANGED
|
@@ -20,14 +20,24 @@ export {
|
|
|
20
20
|
BpmnRuntimeView,
|
|
21
21
|
type BpmnRuntimeViewProps,
|
|
22
22
|
} from "./BpmnRuntimeView.js";
|
|
23
|
+
export {
|
|
24
|
+
describeRunState,
|
|
25
|
+
markerKey,
|
|
26
|
+
bpmnKey,
|
|
27
|
+
resourceList,
|
|
28
|
+
capEvents,
|
|
29
|
+
} from "./runState.js";
|
|
23
30
|
export {
|
|
24
31
|
JobFailure,
|
|
32
|
+
settleReason,
|
|
33
|
+
unhandledJobTypes,
|
|
25
34
|
type JobHandler,
|
|
26
35
|
type JobResult,
|
|
27
36
|
type AgentHandler,
|
|
28
37
|
type DispatchOptions,
|
|
29
38
|
type DispatchResult,
|
|
30
39
|
type RoundResult,
|
|
40
|
+
type SettleReason,
|
|
31
41
|
} from "@nanobpm/bojtos-kit";
|
|
32
42
|
export type {
|
|
33
43
|
BojtosSession,
|
|
@@ -37,7 +47,14 @@ export type {
|
|
|
37
47
|
ActivatedJob,
|
|
38
48
|
IncidentDto,
|
|
39
49
|
TimerDto,
|
|
50
|
+
UserTaskDto,
|
|
51
|
+
MessageSubscriptionDto,
|
|
52
|
+
SignalSubscriptionDto,
|
|
53
|
+
ElementStatDto,
|
|
54
|
+
SequenceFlowDto,
|
|
55
|
+
DecisionInstanceDto,
|
|
40
56
|
ActiveEl,
|
|
57
|
+
ActivateInstruction,
|
|
41
58
|
AgentActivation,
|
|
42
59
|
AgentResult,
|
|
43
60
|
WasmEvent,
|
package/src/runState.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers behind the React binding's decisions.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the component and the hook — and out of any module that imports
|
|
5
|
+
* bpmn-js — so they can be tested without a DOM or the peer dependency. The
|
|
6
|
+
* component and hook hold rendering and lifecycle; what counts as a change, how
|
|
7
|
+
* a run reads aloud, which resources to deploy and how far to trim the log are
|
|
8
|
+
* decisions, and decisions are worth testing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Stable key for a marker set, so unchanged ids don't re-paint the diagram.
|
|
13
|
+
*
|
|
14
|
+
* Relies on BPMN element ids being XML NCNames (so they can't contain `,` or
|
|
15
|
+
* `|`) — that invariant is what keeps the two-field join collision-free. If ids
|
|
16
|
+
* could contain the delimiters, `["a,b"],[]` and `["a"],["b"]`-style pairs
|
|
17
|
+
* would key alike.
|
|
18
|
+
*/
|
|
19
|
+
export function markerKey(activeIds: string[], incidentIds: string[]): string {
|
|
20
|
+
return `${activeIds.join(",")}|${incidentIds.join(",")}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Content key for the `bpmn` prop, so a fresh array identity each render doesn't
|
|
25
|
+
* re-create the engine but a real content change still does. Only the array
|
|
26
|
+
* case is serialized (with a boundary-preserving `JSON.stringify`, not a
|
|
27
|
+
* `join` — a delimiter join lets two different resource arrays collapse to one
|
|
28
|
+
* key when a resource borders/contains the delimiter, silently missing a real
|
|
29
|
+
* change). A lone string can't have array-boundary collisions and React deps
|
|
30
|
+
* already compare strings by value, so it passes through untouched — no needless
|
|
31
|
+
* re-walk of potentially large BPMN XML each render.
|
|
32
|
+
*/
|
|
33
|
+
export function bpmnKey(bpmn: string | string[]): string {
|
|
34
|
+
return Array.isArray(bpmn) ? JSON.stringify(bpmn) : bpmn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Normalize the `bpmn` prop to an ordered resource list for deployment. Deploy
|
|
39
|
+
* order is significant: a later resource can reference an earlier one (a call
|
|
40
|
+
* activity's child), so the array order is preserved verbatim.
|
|
41
|
+
*/
|
|
42
|
+
export function resourceList(bpmn: string | string[]): string[] {
|
|
43
|
+
return Array.isArray(bpmn) ? bpmn : [bpmn];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Trim an event log to the consumer's cap, keeping the most recent events.
|
|
48
|
+
* `undefined` or a negative cap means "no cap"; `0` means "keep nothing". The
|
|
49
|
+
* input array is never mutated.
|
|
50
|
+
*/
|
|
51
|
+
export function capEvents<T>(all: T[], cap: number | undefined): T[] {
|
|
52
|
+
return cap !== undefined && cap >= 0 && all.length > cap
|
|
53
|
+
? all.slice(all.length - cap)
|
|
54
|
+
: all;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A one-line description of run state, for the diagram's live region. The token
|
|
59
|
+
* and incident highlights are purely visual; this is the same information as
|
|
60
|
+
* text.
|
|
61
|
+
*/
|
|
62
|
+
export function describeRunState(
|
|
63
|
+
activeIds: string[],
|
|
64
|
+
incidentIds: string[],
|
|
65
|
+
name: (id: string) => string = (id) => id,
|
|
66
|
+
): string {
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
if (activeIds.length) parts.push(`Running: ${activeIds.map(name).join(", ")}`);
|
|
69
|
+
if (incidentIds.length)
|
|
70
|
+
parts.push(`Incident: ${incidentIds.map(name).join(", ")}`);
|
|
71
|
+
return parts.length ? parts.join(". ") : "Nothing running";
|
|
72
|
+
}
|
package/src/useBojtos.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
+
type ActivateInstruction,
|
|
3
4
|
type AgentResult,
|
|
4
5
|
type BojtosSession,
|
|
5
6
|
createBojtosSession,
|
|
@@ -12,13 +13,20 @@ import {
|
|
|
12
13
|
type WasmEvent,
|
|
13
14
|
type WasmSource,
|
|
14
15
|
} from "@nanobpm/bojtos-kit";
|
|
16
|
+
import { bpmnKey, capEvents, resourceList } from "./runState.js";
|
|
15
17
|
|
|
16
18
|
/** Lifecycle of the in-browser engine load. */
|
|
17
19
|
export type BojtosPhase = "loading" | "ready" | "error";
|
|
18
20
|
|
|
19
21
|
export interface UseBojtosOptions {
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The BPMN to deploy. Re-deploys on a fresh engine when it changes.
|
|
24
|
+
*
|
|
25
|
+
* Pass an array to deploy several resources into one engine — a called
|
|
26
|
+
* process alongside its parent, say. `processIds` then lists every deployable
|
|
27
|
+
* process across all of them, in deployment order.
|
|
28
|
+
*/
|
|
29
|
+
bpmn: string | string[];
|
|
22
30
|
/**
|
|
23
31
|
* Optional engine wasm source. Pass a `URL` / bytes / `WebAssembly.Module`
|
|
24
32
|
* when the default `import.meta.url` loader can't resolve the binary (the
|
|
@@ -29,6 +37,15 @@ export interface UseBojtosOptions {
|
|
|
29
37
|
* reload the module.
|
|
30
38
|
*/
|
|
31
39
|
wasm?: WasmSource;
|
|
40
|
+
/**
|
|
41
|
+
* Cap the reactive `events` log at the most recent N entries.
|
|
42
|
+
*
|
|
43
|
+
* Every command re-reads the engine's full event log into React state, so a
|
|
44
|
+
* long-running demo copies an ever-growing array on each step. Set this when
|
|
45
|
+
* a page runs for a while and only shows a tail; leave it unset to keep the
|
|
46
|
+
* whole log, which stays the default so existing consumers are unaffected.
|
|
47
|
+
*/
|
|
48
|
+
maxEvents?: number;
|
|
32
49
|
}
|
|
33
50
|
|
|
34
51
|
export interface BojtosControls {
|
|
@@ -66,6 +83,74 @@ export interface BojtosControls {
|
|
|
66
83
|
): Snapshot | null;
|
|
67
84
|
/** Advance the virtual clock. */
|
|
68
85
|
advanceTime(byMs: number): Snapshot | null;
|
|
86
|
+
/**
|
|
87
|
+
* Throw a BPMN business error from a waiting job: interrupts the activity via
|
|
88
|
+
* a matching error boundary/event-subprocess catch, or raises an incident if
|
|
89
|
+
* uncaught. The job is consumed either way.
|
|
90
|
+
*/
|
|
91
|
+
throwError(
|
|
92
|
+
jobKey: string,
|
|
93
|
+
errorCode: string,
|
|
94
|
+
errorMessage: string,
|
|
95
|
+
): Snapshot | null;
|
|
96
|
+
/**
|
|
97
|
+
* Set a job's remaining retries. Used to recover a job parked on a no-retries
|
|
98
|
+
* incident before resolving that incident; does not itself unblock the job.
|
|
99
|
+
*/
|
|
100
|
+
updateRetries(jobKey: string, retries: number): Snapshot | null;
|
|
101
|
+
/**
|
|
102
|
+
* Resolve an open incident by key, retrying the work that failed. Pair with
|
|
103
|
+
* {@link updateRetries} to make a failed job activatable again — the
|
|
104
|
+
* incident/retry loop a demo needs to show recovery.
|
|
105
|
+
*/
|
|
106
|
+
resolveIncident(incidentKey: string): Snapshot | null;
|
|
107
|
+
/**
|
|
108
|
+
* Merge variables into a scope (a process-instance or element-instance key).
|
|
109
|
+
* With `local`, they are written strictly into that scope; otherwise they
|
|
110
|
+
* propagate up to the nearest ancestor defining each name.
|
|
111
|
+
*/
|
|
112
|
+
setVariables(
|
|
113
|
+
scopeKey: string,
|
|
114
|
+
variablesJson: string,
|
|
115
|
+
local: boolean,
|
|
116
|
+
): Snapshot | null;
|
|
117
|
+
/** Broadcast a signal by name to every matching open subscription. */
|
|
118
|
+
broadcastSignal(signalName: string, variablesJson: string): Snapshot | null;
|
|
119
|
+
/** Cancel (terminate) a running process instance. */
|
|
120
|
+
cancelInstance(instanceKey: string): Snapshot | null;
|
|
121
|
+
/**
|
|
122
|
+
* Modify a running instance: terminate element instances and/or activate new
|
|
123
|
+
* ones (Zeebe "modify process instance").
|
|
124
|
+
*/
|
|
125
|
+
modify(
|
|
126
|
+
instanceKey: string,
|
|
127
|
+
activateInstructions: ActivateInstruction[],
|
|
128
|
+
terminateElementInstanceKeys: string[],
|
|
129
|
+
): Snapshot | null;
|
|
130
|
+
/**
|
|
131
|
+
* Complete a waiting user task, merging output variables.
|
|
132
|
+
*
|
|
133
|
+
* A `userTask` produces no job, so the dispatch loop cannot advance one: this
|
|
134
|
+
* is the only way a model with a human step reaches its end event. Drive it
|
|
135
|
+
* from `snapshot.userTasks`.
|
|
136
|
+
*/
|
|
137
|
+
completeUserTask(userTaskKey: string, variablesJson: string): Snapshot | null;
|
|
138
|
+
/**
|
|
139
|
+
* Assign a user task. With `allowOverride` false the command is rejected if
|
|
140
|
+
* the task already has an assignee.
|
|
141
|
+
*/
|
|
142
|
+
assignUserTask(
|
|
143
|
+
userTaskKey: string,
|
|
144
|
+
assignee: string,
|
|
145
|
+
allowOverride: boolean,
|
|
146
|
+
): Snapshot | null;
|
|
147
|
+
/** Clear a user task's assignee. */
|
|
148
|
+
unassignUserTask(userTaskKey: string): Snapshot | null;
|
|
149
|
+
/**
|
|
150
|
+
* Update a user task's attributes from a JSON changeset (`candidateGroups`,
|
|
151
|
+
* `candidateUsers`, `dueDate`, `followUpDate`, `priority`).
|
|
152
|
+
*/
|
|
153
|
+
updateUserTask(userTaskKey: string, changesetJson: string): Snapshot | null;
|
|
69
154
|
/**
|
|
70
155
|
* Run the registered worker handlers until the process settles (activate →
|
|
71
156
|
* handler → complete/fail), then reflect the resulting snapshot/events.
|
|
@@ -92,6 +177,29 @@ export interface BojtosControls {
|
|
|
92
177
|
reset(): void;
|
|
93
178
|
}
|
|
94
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Session members the hook deliberately does not re-export: the deployment
|
|
182
|
+
* lifecycle it owns itself, and the low-level activate primitive the dispatch
|
|
183
|
+
* loop owns.
|
|
184
|
+
*/
|
|
185
|
+
type NotReExported = "deploy" | "free" | "activateJobs";
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Compile-time guard. `useBojtos` keeps its session private, so a command it
|
|
189
|
+
* doesn't re-export is *unreachable* for a consumer rather than merely
|
|
190
|
+
* inconvenient — which is how `completeUserTask` went missing and left any model
|
|
191
|
+
* with a user task unfinishable (#1).
|
|
192
|
+
*
|
|
193
|
+
* Adding a command to {@link BojtosSession} without a binding here now fails the
|
|
194
|
+
* build with the offending name, instead of shipping a hole.
|
|
195
|
+
*/
|
|
196
|
+
type UnboundCommands = Exclude<
|
|
197
|
+
keyof BojtosSession,
|
|
198
|
+
NotReExported | keyof BojtosControls
|
|
199
|
+
>;
|
|
200
|
+
type AssertNever<T extends never> = T;
|
|
201
|
+
type _EverySessionCommandIsBound = AssertNever<UnboundCommands>;
|
|
202
|
+
|
|
95
203
|
/**
|
|
96
204
|
* React binding over a headless {@link BojtosSession}: owns the engine's
|
|
97
205
|
* lifecycle and the reactive `snapshot` / `events` / `processIds` state, and
|
|
@@ -103,7 +211,11 @@ export interface BojtosControls {
|
|
|
103
211
|
* test-run panel is its first consumer (§8 step 2 — dogfooding is the acceptance
|
|
104
212
|
* test).
|
|
105
213
|
*/
|
|
106
|
-
export function useBojtos({
|
|
214
|
+
export function useBojtos({
|
|
215
|
+
bpmn,
|
|
216
|
+
wasm,
|
|
217
|
+
maxEvents,
|
|
218
|
+
}: UseBojtosOptions): BojtosControls {
|
|
107
219
|
const sessionRef = useRef<BojtosSession | null>(null);
|
|
108
220
|
const [phase, setPhase] = useState<BojtosPhase>("loading");
|
|
109
221
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -117,15 +229,34 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
|
|
|
117
229
|
const wasmRef = useRef(wasm);
|
|
118
230
|
wasmRef.current = wasm;
|
|
119
231
|
|
|
232
|
+
// An array prop has a fresh identity every render, which would re-create the
|
|
233
|
+
// engine on each one. Key the deploy effect on the content instead — see
|
|
234
|
+
// `bpmnKey` for why this is a boundary-preserving serialization, not a join.
|
|
235
|
+
const deployKey = bpmnKey(bpmn);
|
|
236
|
+
const bpmnRef = useRef(bpmn);
|
|
237
|
+
bpmnRef.current = bpmn;
|
|
238
|
+
|
|
239
|
+
// Trim the reactive event log when the consumer asked for a cap.
|
|
240
|
+
const maxEventsRef = useRef(maxEvents);
|
|
241
|
+
maxEventsRef.current = maxEvents;
|
|
242
|
+
const readEvents = useCallback((session: BojtosSession): WasmEvent[] => {
|
|
243
|
+
return capEvents(session.events(), maxEventsRef.current);
|
|
244
|
+
}, []);
|
|
245
|
+
|
|
120
246
|
const deployInto = useCallback(
|
|
121
247
|
(session: BojtosSession) => {
|
|
122
|
-
const
|
|
123
|
-
|
|
248
|
+
const resources = resourceList(bpmnRef.current);
|
|
249
|
+
// Deploy in order, collecting every deployable process id. A later
|
|
250
|
+
// resource can reference an earlier one (a call activity's child).
|
|
251
|
+
const ids: string[] = [];
|
|
252
|
+
for (const xml of resources) ids.push(...session.deploy(xml).processIds);
|
|
253
|
+
setProcessIds(ids);
|
|
124
254
|
setSnapshot(null);
|
|
125
255
|
setEvents([]);
|
|
126
256
|
setError(null);
|
|
127
257
|
},
|
|
128
|
-
|
|
258
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
259
|
+
[deployKey],
|
|
129
260
|
);
|
|
130
261
|
|
|
131
262
|
useEffect(() => {
|
|
@@ -178,7 +309,7 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
|
|
|
178
309
|
try {
|
|
179
310
|
const snap = fn(session);
|
|
180
311
|
setSnapshot(snap);
|
|
181
|
-
setEvents(session
|
|
312
|
+
setEvents(readEvents(session));
|
|
182
313
|
setError(null);
|
|
183
314
|
return snap;
|
|
184
315
|
} catch (e) {
|
|
@@ -218,6 +349,68 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
|
|
|
218
349
|
run((s) => s.correlateMessage(messageName, correlationKey, variablesJson)),
|
|
219
350
|
[run],
|
|
220
351
|
);
|
|
352
|
+
const throwError = useCallback(
|
|
353
|
+
(jobKey: string, errorCode: string, errorMessage: string) =>
|
|
354
|
+
run((s) => s.throwError(jobKey, errorCode, errorMessage)),
|
|
355
|
+
[run],
|
|
356
|
+
);
|
|
357
|
+
const updateRetries = useCallback(
|
|
358
|
+
(jobKey: string, retries: number) =>
|
|
359
|
+
run((s) => s.updateRetries(jobKey, retries)),
|
|
360
|
+
[run],
|
|
361
|
+
);
|
|
362
|
+
const resolveIncident = useCallback(
|
|
363
|
+
(incidentKey: string) => run((s) => s.resolveIncident(incidentKey)),
|
|
364
|
+
[run],
|
|
365
|
+
);
|
|
366
|
+
const setVariables = useCallback(
|
|
367
|
+
(scopeKey: string, variablesJson: string, local: boolean) =>
|
|
368
|
+
run((s) => s.setVariables(scopeKey, variablesJson, local)),
|
|
369
|
+
[run],
|
|
370
|
+
);
|
|
371
|
+
const broadcastSignal = useCallback(
|
|
372
|
+
(signalName: string, variablesJson: string) =>
|
|
373
|
+
run((s) => s.broadcastSignal(signalName, variablesJson)),
|
|
374
|
+
[run],
|
|
375
|
+
);
|
|
376
|
+
const cancelInstance = useCallback(
|
|
377
|
+
(instanceKey: string) => run((s) => s.cancelInstance(instanceKey)),
|
|
378
|
+
[run],
|
|
379
|
+
);
|
|
380
|
+
const modify = useCallback(
|
|
381
|
+
(
|
|
382
|
+
instanceKey: string,
|
|
383
|
+
activateInstructions: ActivateInstruction[],
|
|
384
|
+
terminateElementInstanceKeys: string[],
|
|
385
|
+
) =>
|
|
386
|
+
run((s) =>
|
|
387
|
+
s.modify(
|
|
388
|
+
instanceKey,
|
|
389
|
+
activateInstructions,
|
|
390
|
+
terminateElementInstanceKeys,
|
|
391
|
+
),
|
|
392
|
+
),
|
|
393
|
+
[run],
|
|
394
|
+
);
|
|
395
|
+
const completeUserTask = useCallback(
|
|
396
|
+
(userTaskKey: string, variablesJson: string) =>
|
|
397
|
+
run((s) => s.completeUserTask(userTaskKey, variablesJson)),
|
|
398
|
+
[run],
|
|
399
|
+
);
|
|
400
|
+
const assignUserTask = useCallback(
|
|
401
|
+
(userTaskKey: string, assignee: string, allowOverride: boolean) =>
|
|
402
|
+
run((s) => s.assignUserTask(userTaskKey, assignee, allowOverride)),
|
|
403
|
+
[run],
|
|
404
|
+
);
|
|
405
|
+
const unassignUserTask = useCallback(
|
|
406
|
+
(userTaskKey: string) => run((s) => s.unassignUserTask(userTaskKey)),
|
|
407
|
+
[run],
|
|
408
|
+
);
|
|
409
|
+
const updateUserTask = useCallback(
|
|
410
|
+
(userTaskKey: string, changesetJson: string) =>
|
|
411
|
+
run((s) => s.updateUserTask(userTaskKey, changesetJson)),
|
|
412
|
+
[run],
|
|
413
|
+
);
|
|
221
414
|
|
|
222
415
|
const runWorkers = useCallback(
|
|
223
416
|
async (
|
|
@@ -236,7 +429,7 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
|
|
|
236
429
|
// while we awaited — don't publish stale state or read a freed session.
|
|
237
430
|
if (sessionRef.current !== session) return null;
|
|
238
431
|
setSnapshot(settled);
|
|
239
|
-
setEvents(session
|
|
432
|
+
setEvents(readEvents(session));
|
|
240
433
|
setError(null);
|
|
241
434
|
return settled;
|
|
242
435
|
} catch (e) {
|
|
@@ -244,7 +437,7 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
|
|
|
244
437
|
// Reflect whatever state the engine reached before the drain aborted
|
|
245
438
|
// (e.g. the maxRounds guard) so the view isn't left stale.
|
|
246
439
|
setSnapshot(session.snapshot());
|
|
247
|
-
setEvents(session
|
|
440
|
+
setEvents(readEvents(session));
|
|
248
441
|
setError(String(e));
|
|
249
442
|
return null;
|
|
250
443
|
}
|
|
@@ -264,13 +457,13 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
|
|
|
264
457
|
// Bail if the session was replaced/freed while we awaited the round.
|
|
265
458
|
if (sessionRef.current !== session) return null;
|
|
266
459
|
setSnapshot(round.snapshot);
|
|
267
|
-
setEvents(session
|
|
460
|
+
setEvents(readEvents(session));
|
|
268
461
|
setError(null);
|
|
269
462
|
return round;
|
|
270
463
|
} catch (e) {
|
|
271
464
|
if (sessionRef.current !== session) return null;
|
|
272
465
|
setSnapshot(session.snapshot());
|
|
273
|
-
setEvents(session
|
|
466
|
+
setEvents(readEvents(session));
|
|
274
467
|
setError(String(e));
|
|
275
468
|
return null;
|
|
276
469
|
}
|
|
@@ -304,6 +497,17 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
|
|
|
304
497
|
failJob,
|
|
305
498
|
advanceTime,
|
|
306
499
|
correlateMessage,
|
|
500
|
+
throwError,
|
|
501
|
+
updateRetries,
|
|
502
|
+
resolveIncident,
|
|
503
|
+
setVariables,
|
|
504
|
+
broadcastSignal,
|
|
505
|
+
cancelInstance,
|
|
506
|
+
modify,
|
|
507
|
+
completeUserTask,
|
|
508
|
+
assignUserTask,
|
|
509
|
+
unassignUserTask,
|
|
510
|
+
updateUserTask,
|
|
307
511
|
runWorkers,
|
|
308
512
|
stepWorkers,
|
|
309
513
|
reset,
|