@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 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`.
@@ -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
+ }
@@ -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;