@nanobpm/bojtos-react 0.1.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 +56 -0
- package/dist/Bojtos.d.ts +51 -0
- package/dist/Bojtos.js +154 -0
- package/dist/BpmnRuntimeView.d.ts +23 -0
- package/dist/BpmnRuntimeView.js +109 -0
- package/dist/examples/orderFulfillment.d.ts +17 -0
- package/dist/examples/orderFulfillment.js +88 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +10 -0
- package/dist/useBojtos.d.ts +70 -0
- package/dist/useBojtos.js +177 -0
- package/package.json +47 -0
- package/src/Bojtos.tsx +263 -0
- package/src/BpmnRuntimeView.tsx +148 -0
- package/src/bpmn-js.d.ts +18 -0
- package/src/examples/orderFulfillment.tsx +98 -0
- package/src/index.ts +41 -0
- package/src/useBojtos.ts +295 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @nanobpm/bojtos-react
|
|
2
|
+
|
|
3
|
+
React binding for the **Bojtos** in-browser BPMN demo framework
|
|
4
|
+
([ADR 0043](../docs/adr/0043-bojtos-demo-framework.md)), built on
|
|
5
|
+
[`@nanobpm/bojtos-kit`](../bojtos-kit).
|
|
6
|
+
|
|
7
|
+
- **`useBojtos({ bpmn })`** — owns the engine session and the reactive
|
|
8
|
+
`snapshot` / `events` / `processIds` state, and exposes the engine commands
|
|
9
|
+
(`createInstance`, `completeJob`, `failJob`, `advanceTime`, `reset`).
|
|
10
|
+
- **`<BpmnRuntimeView xml activeIds incidentIds />`** — the live diagram: it
|
|
11
|
+
imports the XML once and updates token (`nano-active`) / incident
|
|
12
|
+
(`nano-incident`) markers in place, so zoom/scroll survive stepping.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @nanobpm/bojtos-react react react-dom bpmn-js
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`@nanobpm/bojtos-kit` and `@nanobpm/engine-wasm` (the wasm engine) are pulled in
|
|
21
|
+
transitively — you only add the `react` / `bpmn-js` peers yourself. This is all
|
|
22
|
+
you need to build your own Bojtos demo outside this repo; see the usage snippet
|
|
23
|
+
below.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { useBojtos, BpmnRuntimeView } from "@nanobpm/bojtos-react";
|
|
29
|
+
import "bpmn-js/dist/assets/diagram-js.css";
|
|
30
|
+
import "bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css";
|
|
31
|
+
|
|
32
|
+
function Demo({ bpmn }: { bpmn: string }) {
|
|
33
|
+
const run = useBojtos({ bpmn });
|
|
34
|
+
const snap = run.snapshot;
|
|
35
|
+
return (
|
|
36
|
+
<BpmnRuntimeView
|
|
37
|
+
xml={bpmn}
|
|
38
|
+
activeIds={snap?.activeElementIds ?? []}
|
|
39
|
+
incidentIds={snap?.incidentElementIds ?? []}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Peer requirements
|
|
46
|
+
|
|
47
|
+
`react` and `bpmn-js` are peer dependencies (the consumer already has them). The
|
|
48
|
+
consumer must import bpmn-js's diagram CSS once and provide the `.nano-active` /
|
|
49
|
+
`.nano-incident` marker styles.
|
|
50
|
+
|
|
51
|
+
## Build
|
|
52
|
+
|
|
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 committed
|
|
55
|
+
so `file:` consumers and CI need no build-on-install step. Regenerate with
|
|
56
|
+
`npm run build`.
|
package/dist/Bojtos.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { JobHandler, WasmEvent } from "@nanobpm/bojtos-kit";
|
|
2
|
+
/** A single engine event handed to `onTrace` as the simulation runs. */
|
|
3
|
+
export type TraceEvent = WasmEvent;
|
|
4
|
+
export interface BojtosProps {
|
|
5
|
+
/** The BPMN diagram XML to run. */
|
|
6
|
+
bpmn: string;
|
|
7
|
+
/**
|
|
8
|
+
* The in-browser workers, keyed by the model's job type (task definition
|
|
9
|
+
* type). Each handler receives the activated job (with the instance's live
|
|
10
|
+
* variables) and returns the variables to merge on completion — or throws to
|
|
11
|
+
* fail the job. This is the code a demo author edits to shape the run.
|
|
12
|
+
*/
|
|
13
|
+
workers: Record<string, JobHandler>;
|
|
14
|
+
/** Initial instance variables (the starting payload). Defaults to `{}`. */
|
|
15
|
+
seed?: Record<string, unknown>;
|
|
16
|
+
/** Start the instance and run the workers automatically once ready. */
|
|
17
|
+
autoplay?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Milliseconds between dispatch rounds while playing (default 700). The pause
|
|
20
|
+
* is what makes the token visibly hop task-to-task instead of settling
|
|
21
|
+
* instantly.
|
|
22
|
+
*/
|
|
23
|
+
stepDelayMs?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Which deployed process to start. Defaults to the first process in the
|
|
26
|
+
* diagram — set this only for a multi-process `.bpmn`.
|
|
27
|
+
*/
|
|
28
|
+
processId?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Optional engine wasm URL for bundlers where the default `import.meta.url`
|
|
31
|
+
* loader can't resolve the binary (ADR 0043 §3).
|
|
32
|
+
*/
|
|
33
|
+
wasmUrl?: string;
|
|
34
|
+
/** Called for every engine event as the simulation advances. */
|
|
35
|
+
onTrace?: (event: TraceEvent) => void;
|
|
36
|
+
/** Optional class for the outer container. */
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The turnkey Bojtos demo component (ADR 0043 §2): drop in a `bpmn` diagram and
|
|
41
|
+
* a map of in-browser `workers`, and it renders the live token/incident diagram
|
|
42
|
+
* beside the running variable payload, driving the "activate → handler →
|
|
43
|
+
* complete/fail" loop so you watch the token advance and the payload mutate as
|
|
44
|
+
* each worker runs.
|
|
45
|
+
*
|
|
46
|
+
* The consuming app must load bpmn-js's diagram CSS once
|
|
47
|
+
* (`bpmn-js/dist/assets/diagram-js.css` and
|
|
48
|
+
* `.../bpmn-font/css/bpmn-embedded.css`); the token/incident marker styles are
|
|
49
|
+
* injected here.
|
|
50
|
+
*/
|
|
51
|
+
export declare function Bojtos({ bpmn, workers, seed, autoplay, stepDelayMs, processId, wasmUrl, onTrace, className, }: BojtosProps): import("react").JSX.Element;
|
package/dist/Bojtos.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { BpmnRuntimeView } from "./BpmnRuntimeView.js";
|
|
4
|
+
import { useBojtos } from "./useBojtos.js";
|
|
5
|
+
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6
|
+
const MARKER_CSS = `
|
|
7
|
+
.bojtos-diagram .nano-active .djs-visual > :nth-child(1) {
|
|
8
|
+
stroke: #10b981 !important;
|
|
9
|
+
stroke-width: 3px !important;
|
|
10
|
+
}
|
|
11
|
+
.bojtos-diagram .nano-incident .djs-visual > :nth-child(1) {
|
|
12
|
+
stroke: #ef4444 !important;
|
|
13
|
+
stroke-width: 3px !important;
|
|
14
|
+
fill: #fee2e2 !important;
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
/**
|
|
18
|
+
* The turnkey Bojtos demo component (ADR 0043 §2): drop in a `bpmn` diagram and
|
|
19
|
+
* a map of in-browser `workers`, and it renders the live token/incident diagram
|
|
20
|
+
* beside the running variable payload, driving the "activate → handler →
|
|
21
|
+
* complete/fail" loop so you watch the token advance and the payload mutate as
|
|
22
|
+
* each worker runs.
|
|
23
|
+
*
|
|
24
|
+
* The consuming app must load bpmn-js's diagram CSS once
|
|
25
|
+
* (`bpmn-js/dist/assets/diagram-js.css` and
|
|
26
|
+
* `.../bpmn-font/css/bpmn-embedded.css`); the token/incident marker styles are
|
|
27
|
+
* injected here.
|
|
28
|
+
*/
|
|
29
|
+
export function Bojtos({ bpmn, workers, seed, autoplay, stepDelayMs = 700, processId, wasmUrl, onTrace, className, }) {
|
|
30
|
+
const { phase, error, processIds, snapshot, events, createInstance, stepWorkers, reset, } = useBojtos({ bpmn, wasm: wasmUrl });
|
|
31
|
+
const [playing, setPlaying] = useState(false);
|
|
32
|
+
const playingRef = useRef(false);
|
|
33
|
+
const startedRef = useRef(false);
|
|
34
|
+
// Tracks whether the component is still mounted, so the async play loop (which
|
|
35
|
+
// can outlive an unmount while awaiting `stepWorkers()` / `delay()`) neither
|
|
36
|
+
// sets state on an unmounted component nor keeps driving a freed session.
|
|
37
|
+
const mountedRef = useRef(true);
|
|
38
|
+
useEffect(() => () => {
|
|
39
|
+
mountedRef.current = false;
|
|
40
|
+
playingRef.current = false;
|
|
41
|
+
}, []);
|
|
42
|
+
// Keep the object/function props in refs so the play loop and the autoplay
|
|
43
|
+
// effect don't churn (or re-fire) when a parent re-renders with fresh
|
|
44
|
+
// identities for `workers` / `seed` / `onTrace`.
|
|
45
|
+
const workersRef = useRef(workers);
|
|
46
|
+
workersRef.current = workers;
|
|
47
|
+
const seedRef = useRef(seed);
|
|
48
|
+
seedRef.current = seed;
|
|
49
|
+
const onTraceRef = useRef(onTrace);
|
|
50
|
+
onTraceRef.current = onTrace;
|
|
51
|
+
// A fresh engine (bpmn change / reset drops back to `loading`) clears the
|
|
52
|
+
// "instance created" latch and stops any in-flight play loop.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (phase === "loading") {
|
|
55
|
+
startedRef.current = false;
|
|
56
|
+
playingRef.current = false;
|
|
57
|
+
setPlaying(false);
|
|
58
|
+
}
|
|
59
|
+
}, [phase]);
|
|
60
|
+
// Forward every newly-appended engine event to `onTrace`.
|
|
61
|
+
const emittedRef = useRef(0);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const cb = onTraceRef.current;
|
|
64
|
+
if (cb) {
|
|
65
|
+
for (let i = emittedRef.current; i < events.length; i++)
|
|
66
|
+
cb(events[i]);
|
|
67
|
+
}
|
|
68
|
+
emittedRef.current = events.length;
|
|
69
|
+
}, [events]);
|
|
70
|
+
const ensureStarted = useCallback(() => {
|
|
71
|
+
if (startedRef.current)
|
|
72
|
+
return true;
|
|
73
|
+
const target = processId ?? processIds[0];
|
|
74
|
+
if (!target)
|
|
75
|
+
return false;
|
|
76
|
+
// Only latch once the instance actually started — a failed createInstance
|
|
77
|
+
// (returns null, e.g. an engine error) must stay retryable rather than
|
|
78
|
+
// wedging the demo in a non-started state.
|
|
79
|
+
if (!createInstance(target, JSON.stringify(seedRef.current ?? {}))) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
startedRef.current = true;
|
|
83
|
+
return true;
|
|
84
|
+
}, [createInstance, processId, processIds]);
|
|
85
|
+
const step = useCallback(async () => {
|
|
86
|
+
if (phase !== "ready")
|
|
87
|
+
return;
|
|
88
|
+
if (!ensureStarted())
|
|
89
|
+
return;
|
|
90
|
+
await stepWorkers(workersRef.current);
|
|
91
|
+
}, [phase, ensureStarted, stepWorkers]);
|
|
92
|
+
const play = useCallback(async () => {
|
|
93
|
+
if (phase !== "ready" || playingRef.current)
|
|
94
|
+
return;
|
|
95
|
+
if (!ensureStarted())
|
|
96
|
+
return;
|
|
97
|
+
playingRef.current = true;
|
|
98
|
+
setPlaying(true);
|
|
99
|
+
try {
|
|
100
|
+
while (playingRef.current) {
|
|
101
|
+
const round = await stepWorkers(workersRef.current);
|
|
102
|
+
if (!round || round.handled === 0)
|
|
103
|
+
break;
|
|
104
|
+
await delay(stepDelayMs);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
playingRef.current = false;
|
|
109
|
+
if (mountedRef.current)
|
|
110
|
+
setPlaying(false);
|
|
111
|
+
}
|
|
112
|
+
}, [phase, ensureStarted, stepWorkers, stepDelayMs]);
|
|
113
|
+
const pause = useCallback(() => {
|
|
114
|
+
playingRef.current = false;
|
|
115
|
+
setPlaying(false);
|
|
116
|
+
}, []);
|
|
117
|
+
const restart = useCallback(() => {
|
|
118
|
+
playingRef.current = false;
|
|
119
|
+
setPlaying(false);
|
|
120
|
+
startedRef.current = false;
|
|
121
|
+
reset();
|
|
122
|
+
}, [reset]);
|
|
123
|
+
// Autoplay once, when the engine first becomes ready.
|
|
124
|
+
const autoplayedRef = useRef(false);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (autoplay && phase === "ready" && !autoplayedRef.current) {
|
|
127
|
+
autoplayedRef.current = true;
|
|
128
|
+
void play();
|
|
129
|
+
}
|
|
130
|
+
if (phase === "loading")
|
|
131
|
+
autoplayedRef.current = false;
|
|
132
|
+
}, [autoplay, phase, play]);
|
|
133
|
+
const ready = phase === "ready";
|
|
134
|
+
const instance = snapshot?.instances[0];
|
|
135
|
+
const variables = instance?.variables ?? {};
|
|
136
|
+
return (_jsxs("div", { className: className, style: { display: "flex", flexDirection: "column", gap: 8, minHeight: 320 }, children: [_jsx("style", { children: MARKER_CSS }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("button", { type: "button", onClick: play, disabled: !ready || playing, children: "\u25B6 Play" }), _jsx("button", { type: "button", onClick: pause, disabled: !playing, children: "\u23F8 Pause" }), _jsx("button", { type: "button", onClick: step, disabled: !ready || playing, children: "\u23ED Step" }), _jsx("button", { type: "button", onClick: restart, disabled: !ready, children: "\u21BA Reset" }), _jsx("span", { style: { marginLeft: "auto", fontSize: 12, opacity: 0.7 }, children: error
|
|
137
|
+
? `error: ${error}`
|
|
138
|
+
: phase === "loading"
|
|
139
|
+
? "loading engine…"
|
|
140
|
+
: instance
|
|
141
|
+
? instance.completed
|
|
142
|
+
? "completed"
|
|
143
|
+
: "running"
|
|
144
|
+
: "ready" })] }), _jsxs("div", { style: { display: "flex", gap: 8, flex: 1, minHeight: 280 }, children: [_jsx("div", { className: "bojtos-diagram", style: { flex: 2, border: "1px solid #e5e7eb", borderRadius: 6 }, children: _jsx(BpmnRuntimeView, { xml: bpmn, activeIds: snapshot?.activeElementIds ?? [], incidentIds: snapshot?.incidentElementIds ?? [] }) }), _jsxs("div", { style: {
|
|
145
|
+
flex: 1,
|
|
146
|
+
minWidth: 200,
|
|
147
|
+
border: "1px solid #e5e7eb",
|
|
148
|
+
borderRadius: 6,
|
|
149
|
+
padding: 8,
|
|
150
|
+
overflow: "auto",
|
|
151
|
+
font: "12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
152
|
+
background: "#f9fafb",
|
|
153
|
+
}, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 4 }, children: "Variables" }), _jsx("pre", { style: { margin: 0, whiteSpace: "pre-wrap" }, children: JSON.stringify(variables, null, 2) })] })] })] }));
|
|
154
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface BpmnRuntimeViewProps {
|
|
2
|
+
/** The diagram XML to render. */
|
|
3
|
+
xml: string;
|
|
4
|
+
/** Element ids to highlight as active (token) — marker class `nano-active`. */
|
|
5
|
+
activeIds: string[];
|
|
6
|
+
/** Element ids to highlight as incidents — marker class `nano-incident`. */
|
|
7
|
+
incidentIds: string[];
|
|
8
|
+
/** Optional class for the container element (it always fills its parent). */
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read-only diagram that imports the XML once and updates token/incident markers
|
|
13
|
+
* in place (no re-import, so the zoom/scroll position is preserved while
|
|
14
|
+
* stepping through the simulation). This is the token-movement half of the
|
|
15
|
+
* Bojtos visual contract (ADR 0043 §4): drive `activeIds` / `incidentIds` from a
|
|
16
|
+
* session snapshot's `activeElementIds` / `incidentElementIds`.
|
|
17
|
+
*
|
|
18
|
+
* The consumer must load bpmn-js's diagram CSS (`bpmn-js/dist/assets/
|
|
19
|
+
* diagram-js.css` and `.../bpmn-font/css/bpmn-embedded.css`) once in the app,
|
|
20
|
+
* and provide the `.nano-active` / `.nano-incident` marker styles plus a
|
|
21
|
+
* `.nano-token` style for the token badge overlaid on each active element.
|
|
22
|
+
*/
|
|
23
|
+
export declare function BpmnRuntimeView({ xml, activeIds, incidentIds, className, }: BpmnRuntimeViewProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import NavigatedViewer from "bpmn-js/lib/NavigatedViewer";
|
|
4
|
+
/**
|
|
5
|
+
* Read-only diagram that imports the XML once and updates token/incident markers
|
|
6
|
+
* in place (no re-import, so the zoom/scroll position is preserved while
|
|
7
|
+
* stepping through the simulation). This is the token-movement half of the
|
|
8
|
+
* Bojtos visual contract (ADR 0043 §4): drive `activeIds` / `incidentIds` from a
|
|
9
|
+
* session snapshot's `activeElementIds` / `incidentElementIds`.
|
|
10
|
+
*
|
|
11
|
+
* The consumer must load bpmn-js's diagram CSS (`bpmn-js/dist/assets/
|
|
12
|
+
* diagram-js.css` and `.../bpmn-font/css/bpmn-embedded.css`) once in the app,
|
|
13
|
+
* and provide the `.nano-active` / `.nano-incident` marker styles plus a
|
|
14
|
+
* `.nano-token` style for the token badge overlaid on each active element.
|
|
15
|
+
*/
|
|
16
|
+
export function BpmnRuntimeView({ xml, activeIds, incidentIds, className, }) {
|
|
17
|
+
const containerRef = useRef(null);
|
|
18
|
+
const viewerRef = useRef(null);
|
|
19
|
+
const importedRef = useRef(false);
|
|
20
|
+
const markedRef = useRef([]);
|
|
21
|
+
const tokenOverlaysRef = useRef([]);
|
|
22
|
+
// Track the latest ids in a ref so the post-import `applyMarkers()` (fired from
|
|
23
|
+
// the `[xml]` effect's async `.then`) uses current values, not the ids that
|
|
24
|
+
// were current when the import started — otherwise ids changing mid-import
|
|
25
|
+
// would leave the diagram unmarked until the next change.
|
|
26
|
+
const idsRef = useRef({ activeIds, incidentIds });
|
|
27
|
+
idsRef.current = { activeIds, incidentIds };
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!containerRef.current)
|
|
30
|
+
return;
|
|
31
|
+
const viewer = new NavigatedViewer({ container: containerRef.current });
|
|
32
|
+
viewerRef.current = viewer;
|
|
33
|
+
importedRef.current = false;
|
|
34
|
+
viewer
|
|
35
|
+
.importXML(xml)
|
|
36
|
+
.then(() => {
|
|
37
|
+
viewer.get("canvas").zoom("fit-viewport");
|
|
38
|
+
importedRef.current = true;
|
|
39
|
+
applyMarkers();
|
|
40
|
+
})
|
|
41
|
+
.catch(() => {
|
|
42
|
+
/* malformed XML — leave blank */
|
|
43
|
+
});
|
|
44
|
+
return () => {
|
|
45
|
+
viewer.destroy();
|
|
46
|
+
viewerRef.current = null;
|
|
47
|
+
};
|
|
48
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
+
}, [xml]);
|
|
50
|
+
function applyMarkers() {
|
|
51
|
+
const viewer = viewerRef.current;
|
|
52
|
+
if (!viewer || !importedRef.current)
|
|
53
|
+
return;
|
|
54
|
+
const canvas = viewer.get("canvas");
|
|
55
|
+
for (const { id, cls } of markedRef.current) {
|
|
56
|
+
try {
|
|
57
|
+
canvas.removeMarker(id, cls);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* ignore */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const next = [];
|
|
64
|
+
for (const id of idsRef.current.activeIds)
|
|
65
|
+
next.push({ id, cls: "nano-active" });
|
|
66
|
+
for (const id of idsRef.current.incidentIds)
|
|
67
|
+
next.push({ id, cls: "nano-incident" });
|
|
68
|
+
for (const { id, cls } of next) {
|
|
69
|
+
try {
|
|
70
|
+
canvas.addMarker(id, cls);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* element not in this diagram */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
markedRef.current = next;
|
|
77
|
+
// A visible token badge on each active element: an explicit "token is here"
|
|
78
|
+
// marker so movement reads clearly even when a class-only highlight is too
|
|
79
|
+
// subtle. Overlays are removed/re-added each update so the token hops with
|
|
80
|
+
// the frontier.
|
|
81
|
+
const overlays = viewer.get("overlays");
|
|
82
|
+
for (const id of tokenOverlaysRef.current) {
|
|
83
|
+
try {
|
|
84
|
+
overlays.remove(id);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* ignore */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const nextOverlays = [];
|
|
91
|
+
for (const id of idsRef.current.activeIds) {
|
|
92
|
+
try {
|
|
93
|
+
nextOverlays.push(overlays.add(id, {
|
|
94
|
+
position: { top: -12, left: -12 },
|
|
95
|
+
html: '<div class="nano-token" aria-hidden="true"></div>',
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* element not in this diagram */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
tokenOverlaysRef.current = nextOverlays;
|
|
103
|
+
}
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
applyMarkers();
|
|
106
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
|
+
}, [activeIds, incidentIds]);
|
|
108
|
+
return (_jsx("div", { ref: containerRef, className: className, style: { width: "100%", height: "100%" } }));
|
|
109
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { JobHandler } from "@nanobpm/bojtos-kit";
|
|
2
|
+
/**
|
|
3
|
+
* A laid-out order-fulfillment diagram: start → reserve stock (inventory) →
|
|
4
|
+
* charge card (payment) → ship (shipping) → done. Unlike the headless engine
|
|
5
|
+
* fixtures, this carries `bpmndi` diagram-interchange so bpmn-js actually
|
|
6
|
+
* renders the shapes for the token to walk across.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ORDER_FULFILLMENT_BPMN = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<bpmn:definitions xmlns:bpmn=\"http://www.omg.org/spec/BPMN/20100524/MODEL\" xmlns:bpmndi=\"http://www.omg.org/spec/BPMN/20100524/DI\" xmlns:dc=\"http://www.omg.org/spec/DD/20100524/DC\" xmlns:di=\"http://www.omg.org/spec/DD/20100524/DI\" xmlns:zeebe=\"http://camunda.org/schema/zeebe/1.0\" id=\"order-fulfillment-defs\" targetNamespace=\"http://bpmn.io/schema/bpmn\">\n <bpmn:process id=\"order-fulfillment\" isExecutable=\"true\">\n <bpmn:startEvent id=\"start\" name=\"Order placed\">\n <bpmn:outgoing>f1</bpmn:outgoing>\n </bpmn:startEvent>\n <bpmn:serviceTask id=\"reserve\" name=\"Reserve stock\">\n <bpmn:extensionElements><zeebe:taskDefinition type=\"inventory\" /></bpmn:extensionElements>\n <bpmn:incoming>f1</bpmn:incoming>\n <bpmn:outgoing>f2</bpmn:outgoing>\n </bpmn:serviceTask>\n <bpmn:serviceTask id=\"charge\" name=\"Charge card\">\n <bpmn:extensionElements><zeebe:taskDefinition type=\"payment\" /></bpmn:extensionElements>\n <bpmn:incoming>f2</bpmn:incoming>\n <bpmn:outgoing>f3</bpmn:outgoing>\n </bpmn:serviceTask>\n <bpmn:serviceTask id=\"ship\" name=\"Ship\">\n <bpmn:extensionElements><zeebe:taskDefinition type=\"shipping\" /></bpmn:extensionElements>\n <bpmn:incoming>f3</bpmn:incoming>\n <bpmn:outgoing>f4</bpmn:outgoing>\n </bpmn:serviceTask>\n <bpmn:endEvent id=\"done\" name=\"Fulfilled\">\n <bpmn:incoming>f4</bpmn:incoming>\n </bpmn:endEvent>\n <bpmn:sequenceFlow id=\"f1\" sourceRef=\"start\" targetRef=\"reserve\" />\n <bpmn:sequenceFlow id=\"f2\" sourceRef=\"reserve\" targetRef=\"charge\" />\n <bpmn:sequenceFlow id=\"f3\" sourceRef=\"charge\" targetRef=\"ship\" />\n <bpmn:sequenceFlow id=\"f4\" sourceRef=\"ship\" targetRef=\"done\" />\n </bpmn:process>\n <bpmndi:BPMNDiagram id=\"diagram\">\n <bpmndi:BPMNPlane id=\"plane\" bpmnElement=\"order-fulfillment\">\n <bpmndi:BPMNShape id=\"start_di\" bpmnElement=\"start\">\n <dc:Bounds x=\"150\" y=\"102\" width=\"36\" height=\"36\" />\n </bpmndi:BPMNShape>\n <bpmndi:BPMNShape id=\"reserve_di\" bpmnElement=\"reserve\">\n <dc:Bounds x=\"240\" y=\"80\" width=\"100\" height=\"80\" />\n </bpmndi:BPMNShape>\n <bpmndi:BPMNShape id=\"charge_di\" bpmnElement=\"charge\">\n <dc:Bounds x=\"400\" y=\"80\" width=\"100\" height=\"80\" />\n </bpmndi:BPMNShape>\n <bpmndi:BPMNShape id=\"ship_di\" bpmnElement=\"ship\">\n <dc:Bounds x=\"560\" y=\"80\" width=\"100\" height=\"80\" />\n </bpmndi:BPMNShape>\n <bpmndi:BPMNShape id=\"done_di\" bpmnElement=\"done\">\n <dc:Bounds x=\"720\" y=\"102\" width=\"36\" height=\"36\" />\n </bpmndi:BPMNShape>\n <bpmndi:BPMNEdge id=\"f1_di\" bpmnElement=\"f1\">\n <di:waypoint x=\"186\" y=\"120\" />\n <di:waypoint x=\"240\" y=\"120\" />\n </bpmndi:BPMNEdge>\n <bpmndi:BPMNEdge id=\"f2_di\" bpmnElement=\"f2\">\n <di:waypoint x=\"340\" y=\"120\" />\n <di:waypoint x=\"400\" y=\"120\" />\n </bpmndi:BPMNEdge>\n <bpmndi:BPMNEdge id=\"f3_di\" bpmnElement=\"f3\">\n <di:waypoint x=\"500\" y=\"120\" />\n <di:waypoint x=\"560\" y=\"120\" />\n </bpmndi:BPMNEdge>\n <bpmndi:BPMNEdge id=\"f4_di\" bpmnElement=\"f4\">\n <di:waypoint x=\"660\" y=\"120\" />\n <di:waypoint x=\"720\" y=\"120\" />\n </bpmndi:BPMNEdge>\n </bpmndi:BPMNPlane>\n </bpmndi:BPMNDiagram>\n</bpmn:definitions>";
|
|
9
|
+
/**
|
|
10
|
+
* The in-browser workers for {@link ORDER_FULFILLMENT_BPMN}. Each reads the
|
|
11
|
+
* instance's live payload and returns the variables it adds — edit a handler and
|
|
12
|
+
* re-run to watch the payload change (ADR 0043 §8 step 4 makes these boxes
|
|
13
|
+
* editable in the browser).
|
|
14
|
+
*/
|
|
15
|
+
export declare const orderFulfillmentWorkers: Record<string, JobHandler>;
|
|
16
|
+
/** A ready-to-drop-in Bojtos demo — the canonical first example (ADR 0043 §8). */
|
|
17
|
+
export declare function OrderFulfillmentDemo(): import("react").JSX.Element;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Bojtos } from "../Bojtos.js";
|
|
3
|
+
/**
|
|
4
|
+
* A laid-out order-fulfillment diagram: start → reserve stock (inventory) →
|
|
5
|
+
* charge card (payment) → ship (shipping) → done. Unlike the headless engine
|
|
6
|
+
* fixtures, this carries `bpmndi` diagram-interchange so bpmn-js actually
|
|
7
|
+
* renders the shapes for the token to walk across.
|
|
8
|
+
*/
|
|
9
|
+
export const ORDER_FULFILLMENT_BPMN = `<?xml version="1.0" encoding="UTF-8"?>
|
|
10
|
+
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" id="order-fulfillment-defs" targetNamespace="http://bpmn.io/schema/bpmn">
|
|
11
|
+
<bpmn:process id="order-fulfillment" isExecutable="true">
|
|
12
|
+
<bpmn:startEvent id="start" name="Order placed">
|
|
13
|
+
<bpmn:outgoing>f1</bpmn:outgoing>
|
|
14
|
+
</bpmn:startEvent>
|
|
15
|
+
<bpmn:serviceTask id="reserve" name="Reserve stock">
|
|
16
|
+
<bpmn:extensionElements><zeebe:taskDefinition type="inventory" /></bpmn:extensionElements>
|
|
17
|
+
<bpmn:incoming>f1</bpmn:incoming>
|
|
18
|
+
<bpmn:outgoing>f2</bpmn:outgoing>
|
|
19
|
+
</bpmn:serviceTask>
|
|
20
|
+
<bpmn:serviceTask id="charge" name="Charge card">
|
|
21
|
+
<bpmn:extensionElements><zeebe:taskDefinition type="payment" /></bpmn:extensionElements>
|
|
22
|
+
<bpmn:incoming>f2</bpmn:incoming>
|
|
23
|
+
<bpmn:outgoing>f3</bpmn:outgoing>
|
|
24
|
+
</bpmn:serviceTask>
|
|
25
|
+
<bpmn:serviceTask id="ship" name="Ship">
|
|
26
|
+
<bpmn:extensionElements><zeebe:taskDefinition type="shipping" /></bpmn:extensionElements>
|
|
27
|
+
<bpmn:incoming>f3</bpmn:incoming>
|
|
28
|
+
<bpmn:outgoing>f4</bpmn:outgoing>
|
|
29
|
+
</bpmn:serviceTask>
|
|
30
|
+
<bpmn:endEvent id="done" name="Fulfilled">
|
|
31
|
+
<bpmn:incoming>f4</bpmn:incoming>
|
|
32
|
+
</bpmn:endEvent>
|
|
33
|
+
<bpmn:sequenceFlow id="f1" sourceRef="start" targetRef="reserve" />
|
|
34
|
+
<bpmn:sequenceFlow id="f2" sourceRef="reserve" targetRef="charge" />
|
|
35
|
+
<bpmn:sequenceFlow id="f3" sourceRef="charge" targetRef="ship" />
|
|
36
|
+
<bpmn:sequenceFlow id="f4" sourceRef="ship" targetRef="done" />
|
|
37
|
+
</bpmn:process>
|
|
38
|
+
<bpmndi:BPMNDiagram id="diagram">
|
|
39
|
+
<bpmndi:BPMNPlane id="plane" bpmnElement="order-fulfillment">
|
|
40
|
+
<bpmndi:BPMNShape id="start_di" bpmnElement="start">
|
|
41
|
+
<dc:Bounds x="150" y="102" width="36" height="36" />
|
|
42
|
+
</bpmndi:BPMNShape>
|
|
43
|
+
<bpmndi:BPMNShape id="reserve_di" bpmnElement="reserve">
|
|
44
|
+
<dc:Bounds x="240" y="80" width="100" height="80" />
|
|
45
|
+
</bpmndi:BPMNShape>
|
|
46
|
+
<bpmndi:BPMNShape id="charge_di" bpmnElement="charge">
|
|
47
|
+
<dc:Bounds x="400" y="80" width="100" height="80" />
|
|
48
|
+
</bpmndi:BPMNShape>
|
|
49
|
+
<bpmndi:BPMNShape id="ship_di" bpmnElement="ship">
|
|
50
|
+
<dc:Bounds x="560" y="80" width="100" height="80" />
|
|
51
|
+
</bpmndi:BPMNShape>
|
|
52
|
+
<bpmndi:BPMNShape id="done_di" bpmnElement="done">
|
|
53
|
+
<dc:Bounds x="720" y="102" width="36" height="36" />
|
|
54
|
+
</bpmndi:BPMNShape>
|
|
55
|
+
<bpmndi:BPMNEdge id="f1_di" bpmnElement="f1">
|
|
56
|
+
<di:waypoint x="186" y="120" />
|
|
57
|
+
<di:waypoint x="240" y="120" />
|
|
58
|
+
</bpmndi:BPMNEdge>
|
|
59
|
+
<bpmndi:BPMNEdge id="f2_di" bpmnElement="f2">
|
|
60
|
+
<di:waypoint x="340" y="120" />
|
|
61
|
+
<di:waypoint x="400" y="120" />
|
|
62
|
+
</bpmndi:BPMNEdge>
|
|
63
|
+
<bpmndi:BPMNEdge id="f3_di" bpmnElement="f3">
|
|
64
|
+
<di:waypoint x="500" y="120" />
|
|
65
|
+
<di:waypoint x="560" y="120" />
|
|
66
|
+
</bpmndi:BPMNEdge>
|
|
67
|
+
<bpmndi:BPMNEdge id="f4_di" bpmnElement="f4">
|
|
68
|
+
<di:waypoint x="660" y="120" />
|
|
69
|
+
<di:waypoint x="720" y="120" />
|
|
70
|
+
</bpmndi:BPMNEdge>
|
|
71
|
+
</bpmndi:BPMNPlane>
|
|
72
|
+
</bpmndi:BPMNDiagram>
|
|
73
|
+
</bpmn:definitions>`;
|
|
74
|
+
/**
|
|
75
|
+
* The in-browser workers for {@link ORDER_FULFILLMENT_BPMN}. Each reads the
|
|
76
|
+
* instance's live payload and returns the variables it adds — edit a handler and
|
|
77
|
+
* re-run to watch the payload change (ADR 0043 §8 step 4 makes these boxes
|
|
78
|
+
* editable in the browser).
|
|
79
|
+
*/
|
|
80
|
+
export const orderFulfillmentWorkers = {
|
|
81
|
+
inventory: (job) => ({ reserved: true, sku: job.variables.sku }),
|
|
82
|
+
payment: (job) => ({ charged: job.variables.amount ?? 0 }),
|
|
83
|
+
shipping: () => ({ tracking: `1Z${Math.floor(Math.random() * 1e9)}` }),
|
|
84
|
+
};
|
|
85
|
+
/** A ready-to-drop-in Bojtos demo — the canonical first example (ADR 0043 §8). */
|
|
86
|
+
export function OrderFulfillmentDemo() {
|
|
87
|
+
return (_jsx(Bojtos, { bpmn: ORDER_FULFILLMENT_BPMN, workers: orderFulfillmentWorkers, seed: { sku: "WIDGET-1", amount: 4200 }, autoplay: true }));
|
|
88
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { useBojtos, type UseBojtosOptions, type BojtosControls, type BojtosPhase, } from "./useBojtos.js";
|
|
2
|
+
export { Bojtos, type BojtosProps, type TraceEvent } from "./Bojtos.js";
|
|
3
|
+
export { OrderFulfillmentDemo, ORDER_FULFILLMENT_BPMN, orderFulfillmentWorkers, } from "./examples/orderFulfillment.js";
|
|
4
|
+
export { BpmnRuntimeView, type BpmnRuntimeViewProps, } from "./BpmnRuntimeView.js";
|
|
5
|
+
export { JobFailure, type JobHandler, type JobResult, type DispatchOptions, type DispatchResult, type RoundResult, } from "@nanobpm/bojtos-kit";
|
|
6
|
+
export type { BojtosSession, Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, ActiveEl, WasmEvent, } from "@nanobpm/bojtos-kit";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @nanobpm/bojtos-react — the React binding for the Bojtos in-browser BPMN demo
|
|
2
|
+
// framework (ADR 0043). `useBojtos` owns the engine session and reactive state;
|
|
3
|
+
// `<BpmnRuntimeView>` renders the live token/incident diagram. The engine's
|
|
4
|
+
// snapshot/event contract types are re-exported from @nanobpm/bojtos-kit for
|
|
5
|
+
// convenience.
|
|
6
|
+
export { useBojtos, } from "./useBojtos.js";
|
|
7
|
+
export { Bojtos } from "./Bojtos.js";
|
|
8
|
+
export { OrderFulfillmentDemo, ORDER_FULFILLMENT_BPMN, orderFulfillmentWorkers, } from "./examples/orderFulfillment.js";
|
|
9
|
+
export { BpmnRuntimeView, } from "./BpmnRuntimeView.js";
|
|
10
|
+
export { JobFailure, } from "@nanobpm/bojtos-kit";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type DispatchOptions, type JobHandler, type RoundResult, type Snapshot, type WasmEvent, type WasmSource } from "@nanobpm/bojtos-kit";
|
|
2
|
+
/** Lifecycle of the in-browser engine load. */
|
|
3
|
+
export type BojtosPhase = "loading" | "ready" | "error";
|
|
4
|
+
export interface UseBojtosOptions {
|
|
5
|
+
/** The BPMN diagram XML to deploy. Re-deploys on a fresh engine when it changes. */
|
|
6
|
+
bpmn: string;
|
|
7
|
+
/**
|
|
8
|
+
* Optional engine wasm source. Pass a `URL` / bytes / `WebAssembly.Module`
|
|
9
|
+
* when the default `import.meta.url` loader can't resolve the binary (the
|
|
10
|
+
* external-`.wasm` "wasmUrl" mode, or a non-Vite bundler — ADR 0043 §3).
|
|
11
|
+
*
|
|
12
|
+
* Init-time only: the wasm module loads once per page (see `ensureWasm`), so
|
|
13
|
+
* changing `wasm` after the first successful init has no effect — it will not
|
|
14
|
+
* reload the module.
|
|
15
|
+
*/
|
|
16
|
+
wasm?: WasmSource;
|
|
17
|
+
}
|
|
18
|
+
export interface BojtosControls {
|
|
19
|
+
phase: BojtosPhase;
|
|
20
|
+
error: string | null;
|
|
21
|
+
/** Deployable process ids from the current deployment. */
|
|
22
|
+
processIds: string[];
|
|
23
|
+
/** The latest snapshot, or `null` before the first command / after a reset. */
|
|
24
|
+
snapshot: Snapshot | null;
|
|
25
|
+
/** The engine's full event log after the latest command. */
|
|
26
|
+
events: WasmEvent[];
|
|
27
|
+
/** Start an instance; returns the post-run snapshot (with `created`) or null. */
|
|
28
|
+
createInstance(processId: string, variablesJson: string): Snapshot | null;
|
|
29
|
+
/** Complete a waiting job, merging output variables. */
|
|
30
|
+
completeJob(jobKey: string, variablesJson: string): Snapshot | null;
|
|
31
|
+
/** Fail a waiting job (raises an incident with no retries left). */
|
|
32
|
+
failJob(jobKey: string, retries: number, message: string): Snapshot | null;
|
|
33
|
+
/**
|
|
34
|
+
* Correlate a message to an instance parked at a message catch/receive:
|
|
35
|
+
* publishes `messageName` with `correlationKey` and merges `variablesJson`.
|
|
36
|
+
* The in-browser equivalent of an app publishing a message — used to unblock
|
|
37
|
+
* a waiting loop (e.g. urban-pr-review's `review-ready`).
|
|
38
|
+
*/
|
|
39
|
+
correlateMessage(messageName: string, correlationKey: string, variablesJson: string): Snapshot | null;
|
|
40
|
+
/** Advance the virtual clock. */
|
|
41
|
+
advanceTime(byMs: number): Snapshot | null;
|
|
42
|
+
/**
|
|
43
|
+
* Run the registered worker handlers until the process settles (activate →
|
|
44
|
+
* handler → complete/fail), then reflect the resulting snapshot/events.
|
|
45
|
+
* Resolves to the settled snapshot, or null if there is no live session.
|
|
46
|
+
*/
|
|
47
|
+
runWorkers(workers: Record<string, JobHandler>, opts?: DispatchOptions): Promise<Snapshot | null>;
|
|
48
|
+
/**
|
|
49
|
+
* Run a single activate-and-handle pass of the registered workers (one
|
|
50
|
+
* {@link dispatchRound}), reflecting the resulting snapshot/events. Returns
|
|
51
|
+
* how many jobs it handled (0 once the process is quiescent) plus the
|
|
52
|
+
* snapshot, or null if there is no live session — drive it on a timer to
|
|
53
|
+
* animate the token advancing one step at a time.
|
|
54
|
+
*/
|
|
55
|
+
stepWorkers(workers: Record<string, JobHandler>, opts?: DispatchOptions): Promise<RoundResult | null>;
|
|
56
|
+
/** Re-deploy the diagram on the existing engine, clearing run state. */
|
|
57
|
+
reset(): void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* React binding over a headless {@link BojtosSession}: owns the engine's
|
|
61
|
+
* lifecycle and the reactive `snapshot` / `events` / `processIds` state, and
|
|
62
|
+
* exposes the engine commands. The consuming component owns its own form state
|
|
63
|
+
* (selected process, seed vars, per-job output) and drives the visual contract
|
|
64
|
+
* (`<BpmnRuntimeView>` + the variable payload) off `snapshot`.
|
|
65
|
+
*
|
|
66
|
+
* This is the reactive half of the Bojtos public API (ADR 0043 §2); the console
|
|
67
|
+
* test-run panel is its first consumer (§8 step 2 — dogfooding is the acceptance
|
|
68
|
+
* test).
|
|
69
|
+
*/
|
|
70
|
+
export declare function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls;
|