@nanobpm/bojtos-react 0.7.0 → 0.9.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 +45 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/runState.d.ts +18 -0
- package/dist/runState.js +21 -0
- package/dist/useBojtos.d.ts +123 -2
- package/dist/useBojtos.js +91 -18
- package/package.json +5 -2
- package/src/index.ts +16 -0
- package/src/runState.ts +30 -0
- package/src/useBojtos.ts +269 -7
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@ React binding for the **Bojtos** in-browser BPMN demo framework
|
|
|
6
6
|
|
|
7
7
|
- **`useBojtos({ bpmn })`** — owns the engine session and the reactive
|
|
8
8
|
`snapshot` / `events` / `processIds` state, and exposes the engine commands
|
|
9
|
-
(`createInstance`, `completeJob`, `failJob`, `advanceTime`, `reset`).
|
|
9
|
+
(`createInstance`, `completeJob`, `failJob`, `advanceTime`, `reset`). Pass
|
|
10
|
+
`variant: "readmodel"` to also thread the gateway's read channel through the
|
|
11
|
+
hook — see [Read model](#read-model) below.
|
|
10
12
|
- **`<BpmnRuntimeView xml activeIds incidentIds />`** — the live diagram: it
|
|
11
13
|
imports the XML once and updates token (`nano-active`) / incident
|
|
12
14
|
(`nano-incident`) markers in place, so zoom/scroll survive stepping.
|
|
@@ -71,6 +73,48 @@ For an agentic run, emit `TraceEntry` lines from your handlers (with the additiv
|
|
|
71
73
|
unchanged. It never imports `bpmn-js`, so importing it alone won't pull the
|
|
72
74
|
diagram bundle in.
|
|
73
75
|
|
|
76
|
+
## Read model
|
|
77
|
+
|
|
78
|
+
By default `useBojtos` loads the **lean** engine (primary state only). Pass
|
|
79
|
+
`variant: "readmodel"` to load the read-model engine variant instead, which adds
|
|
80
|
+
the gateway's Camunda-parity REST read channel. The returned controls then widen
|
|
81
|
+
from `BojtosControls` to `ReadModelBojtosControls`, exposing five pull queries —
|
|
82
|
+
`searchUserTasks`, `searchProcessInstances`, `searchVariables`, `getFormByKey`,
|
|
83
|
+
`getResourceByKey` — plus a `readModelVersion` counter. The heavier read-model
|
|
84
|
+
binary code-splits in only for `"readmodel"` hooks; a lean hook never downloads
|
|
85
|
+
it.
|
|
86
|
+
|
|
87
|
+
The read queries are **pull** projections of the read model, not part of the
|
|
88
|
+
command→`snapshot` push loop, so they don't land in state on their own. Each read
|
|
89
|
+
method returns `null` until the engine is ready (and on a lean hook), and
|
|
90
|
+
`readModelVersion` bumps after every command / worker round / deploy / reset —
|
|
91
|
+
i.e. whenever the read model may have moved. Make a query reactive with the
|
|
92
|
+
ready-made `useReadModel` selector, which re-runs it keyed on `readModelVersion`:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { useBojtos, useReadModel } from "@nanobpm/bojtos-react";
|
|
96
|
+
|
|
97
|
+
function ReviewInbox({ bpmn }: { bpmn: string }) {
|
|
98
|
+
const run = useBojtos({ bpmn, variant: "readmodel" });
|
|
99
|
+
// Re-runs after every command / round; `?? []` covers the not-ready null.
|
|
100
|
+
const openTasks = useReadModel(
|
|
101
|
+
run,
|
|
102
|
+
(rm) => rm.searchUserTasks('{"state":"CREATED"}')?.items ?? [],
|
|
103
|
+
);
|
|
104
|
+
return (
|
|
105
|
+
<ul>
|
|
106
|
+
{openTasks.map((t) => (
|
|
107
|
+
<li key={t.userTaskKey}>{t.elementId}</li>
|
|
108
|
+
))}
|
|
109
|
+
</ul>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
You can also call the read methods imperatively (e.g. from an event handler)
|
|
115
|
+
whenever you want a one-off answer — `useReadModel` is just the reactive wrapper
|
|
116
|
+
over the same `readModelVersion` signal.
|
|
117
|
+
|
|
74
118
|
## Peer requirements
|
|
75
119
|
|
|
76
120
|
`react` and `bpmn-js` are peer dependencies (the consumer already has them). The
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { useBojtos, type UseBojtosOptions, type BojtosControls, type BojtosPhase, } from "./useBojtos.js";
|
|
1
|
+
export { useBojtos, useReadModel, type UseBojtosOptions, type BojtosControls, type ReadModelBojtosControls, type BojtosPhase, } from "./useBojtos.js";
|
|
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
5
|
export { TraceTimeline, type TraceTimelineProps } from "./TraceTimeline.js";
|
|
6
|
-
export { describeRunState, markerKey, bpmnKey, resourceList, capEvents, } from "./runState.js";
|
|
6
|
+
export { describeRunState, markerKey, bpmnKey, resourceList, capEvents, resolveVariant, selectReadModel, } from "./runState.js";
|
|
7
7
|
export { JobFailure, settleReason, unhandledJobTypes, buildTraceItems, isTraceTurnGroup, foldEngineEvents, traceEntriesToRows, type JobHandler, type JobResult, type AgentHandler, type DispatchOptions, type DispatchResult, type RoundResult, type SettleReason, } from "@nanobpm/bojtos-kit";
|
|
8
|
-
export type { BojtosSession, Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, UserTaskDto, MessageSubscriptionDto, SignalSubscriptionDto, ElementStatDto, SequenceFlowDto, DecisionInstanceDto, ActiveEl, ActivateInstruction, AgentActivation, AgentResult, WasmEvent, TraceRowKind, TraceEntry, TraceRow, TraceTurnGroup, TraceItem, TraceAdapter, } from "@nanobpm/bojtos-kit";
|
|
8
|
+
export type { BojtosSession, ReadModelBojtosSession, EngineVariant, Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, UserTaskDto, MessageSubscriptionDto, SignalSubscriptionDto, ElementStatDto, SequenceFlowDto, DecisionInstanceDto, ActiveEl, ActivateInstruction, AgentActivation, AgentResult, WasmEvent, TraceRowKind, TraceEntry, TraceRow, TraceTurnGroup, TraceItem, TraceAdapter, UserTaskSearchQueryResult, UserTaskResult, ProcessInstanceSearchQueryResult, ProcessInstanceResult, VariableSearchQueryResult, VariableResult, FormResult, ResourceResult, SearchQueryResponse, SearchQueryPageResponse, } from "@nanobpm/bojtos-kit";
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// `<BpmnRuntimeView>` renders the live token/incident diagram. The engine's
|
|
4
4
|
// snapshot/event contract types are re-exported from @nanobpm/bojtos-kit for
|
|
5
5
|
// convenience.
|
|
6
|
-
export { useBojtos, } from "./useBojtos.js";
|
|
6
|
+
export { useBojtos, useReadModel, } 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
10
|
// The shared activity log (#9). Trace-only imports tree-shake bpmn-js out —
|
|
11
11
|
// TraceTimeline imports only the kit + React, never BpmnRuntimeView.
|
|
12
12
|
export { TraceTimeline } from "./TraceTimeline.js";
|
|
13
|
-
export { describeRunState, markerKey, bpmnKey, resourceList, capEvents, } from "./runState.js";
|
|
13
|
+
export { describeRunState, markerKey, bpmnKey, resourceList, capEvents, resolveVariant, selectReadModel, } from "./runState.js";
|
|
14
14
|
export { JobFailure, settleReason, unhandledJobTypes, buildTraceItems, isTraceTurnGroup, foldEngineEvents, traceEntriesToRows, } from "@nanobpm/bojtos-kit";
|
package/dist/runState.d.ts
CHANGED
|
@@ -7,6 +7,24 @@
|
|
|
7
7
|
* a run reads aloud, which resources to deploy and how far to trim the log are
|
|
8
8
|
* decisions, and decisions are worth testing.
|
|
9
9
|
*/
|
|
10
|
+
import type { EngineVariant } from "@nanobpm/bojtos-kit";
|
|
11
|
+
/**
|
|
12
|
+
* The engine variant a hook run should use, defaulting to `"lean"` when the
|
|
13
|
+
* consumer didn't pick one. Keeping the default here (rather than a parameter
|
|
14
|
+
* default) makes the "no `variant` means lean, so existing consumers are
|
|
15
|
+
* unaffected" decision a single testable fact instead of an inline `?? "lean"`
|
|
16
|
+
* scattered through the hook.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveVariant(variant: EngineVariant | undefined): EngineVariant;
|
|
19
|
+
/**
|
|
20
|
+
* Pull a value out of the read-model channel, or `null` when there is no live
|
|
21
|
+
* read-model session (loading, a lean-variant hook, or between a teardown and
|
|
22
|
+
* the next engine). This is the one place the "no session → null, otherwise run
|
|
23
|
+
* the query" decision lives, shared by every reactive read method so a
|
|
24
|
+
* lean-variant call or a mid-load call is a quiet `null` rather than a throw on
|
|
25
|
+
* a missing engine.
|
|
26
|
+
*/
|
|
27
|
+
export declare function selectReadModel<C, T>(channel: C | null, select: (channel: C) => T): T | null;
|
|
10
28
|
/**
|
|
11
29
|
* Stable key for a marker set, so unchanged ids don't re-paint the diagram.
|
|
12
30
|
*
|
package/dist/runState.js
CHANGED
|
@@ -7,6 +7,27 @@
|
|
|
7
7
|
* a run reads aloud, which resources to deploy and how far to trim the log are
|
|
8
8
|
* decisions, and decisions are worth testing.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* The engine variant a hook run should use, defaulting to `"lean"` when the
|
|
12
|
+
* consumer didn't pick one. Keeping the default here (rather than a parameter
|
|
13
|
+
* default) makes the "no `variant` means lean, so existing consumers are
|
|
14
|
+
* unaffected" decision a single testable fact instead of an inline `?? "lean"`
|
|
15
|
+
* scattered through the hook.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveVariant(variant) {
|
|
18
|
+
return variant ?? "lean";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pull a value out of the read-model channel, or `null` when there is no live
|
|
22
|
+
* read-model session (loading, a lean-variant hook, or between a teardown and
|
|
23
|
+
* the next engine). This is the one place the "no session → null, otherwise run
|
|
24
|
+
* the query" decision lives, shared by every reactive read method so a
|
|
25
|
+
* lean-variant call or a mid-load call is a quiet `null` rather than a throw on
|
|
26
|
+
* a missing engine.
|
|
27
|
+
*/
|
|
28
|
+
export function selectReadModel(channel, select) {
|
|
29
|
+
return channel === null ? null : select(channel);
|
|
30
|
+
}
|
|
10
31
|
/**
|
|
11
32
|
* Stable key for a marker set, so unchanged ids don't re-paint the diagram.
|
|
12
33
|
*
|
package/dist/useBojtos.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ActivateInstruction, 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 EngineReadModel, type EngineVariant, type FormResult, type JobHandler, type ProcessInstanceSearchQueryResult, type ResourceResult, type RoundResult, type Snapshot, type UserTaskSearchQueryResult, type VariableSearchQueryResult, 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 {
|
|
@@ -29,6 +29,22 @@ export interface UseBojtosOptions {
|
|
|
29
29
|
* whole log, which stays the default so existing consumers are unaffected.
|
|
30
30
|
*/
|
|
31
31
|
maxEvents?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Which engine variant to load, defaulting to `"lean"` — existing consumers
|
|
34
|
+
* are unaffected. Pass `"readmodel"` to also thread the gateway's
|
|
35
|
+
* Camunda-parity REST read channel through the hook: the returned controls
|
|
36
|
+
* then widen to {@link ReadModelBojtosControls}, exposing `searchUserTasks` /
|
|
37
|
+
* `searchProcessInstances` / `searchVariables` / `getFormByKey` /
|
|
38
|
+
* `getResourceByKey` plus the `readModelVersion` reactivity signal.
|
|
39
|
+
*
|
|
40
|
+
* Init-time only, like `wasm`: the variant is read when the session is first
|
|
41
|
+
* created for a given diagram, so changing it later has no effect until the
|
|
42
|
+
* next `bpmn` change re-creates the engine.
|
|
43
|
+
*
|
|
44
|
+
* The read-model binary is heavier and only code-splits in when this is
|
|
45
|
+
* `"readmodel"` (ADR 0043 §3); a lean hook never downloads it.
|
|
46
|
+
*/
|
|
47
|
+
variant?: EngineVariant;
|
|
32
48
|
}
|
|
33
49
|
export interface BojtosControls {
|
|
34
50
|
phase: BojtosPhase;
|
|
@@ -132,6 +148,78 @@ export interface BojtosControls {
|
|
|
132
148
|
/** Re-deploy the diagram on the existing engine, clearing run state. */
|
|
133
149
|
reset(): void;
|
|
134
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* The {@link BojtosControls} of a `readmodel`-variant hook: the full command
|
|
153
|
+
* surface **plus** reactive access to the gateway's Camunda-parity REST read
|
|
154
|
+
* channel. You get one by passing `variant: "readmodel"` to {@link useBojtos},
|
|
155
|
+
* which widens the return type from `BojtosControls` to this.
|
|
156
|
+
*
|
|
157
|
+
* ## Reactivity model
|
|
158
|
+
*
|
|
159
|
+
* The read queries are **pull** projections of the read model, not part of the
|
|
160
|
+
* command→`snapshot` push loop: `searchUserTasks` et al. answer "what does the
|
|
161
|
+
* read model say *right now*", and there is no single obvious cadence at which
|
|
162
|
+
* to re-run them (a consumer may care about tasks, another about variables, each
|
|
163
|
+
* with its own filter). So rather than eagerly re-running every query after
|
|
164
|
+
* every command and stuffing five results into state, the hook exposes:
|
|
165
|
+
*
|
|
166
|
+
* - the five read methods as **imperative pulls** — call one whenever you want a
|
|
167
|
+
* fresh answer; each returns `null` before the engine is ready rather than
|
|
168
|
+
* throwing, and
|
|
169
|
+
* - {@link readModelVersion}, a counter bumped after **every** command / worker
|
|
170
|
+
* round (i.e. whenever the read model may have moved), so a consumer can make
|
|
171
|
+
* a query reactive by keying a `useMemo`/`useEffect` on it — or just let
|
|
172
|
+
* {@link useReadModel} do exactly that.
|
|
173
|
+
*
|
|
174
|
+
* This keeps the read channel opt-in and filter-agnostic while still landing its
|
|
175
|
+
* results in React state on the consumer's terms.
|
|
176
|
+
*/
|
|
177
|
+
export interface ReadModelBojtosControls extends BojtosControls {
|
|
178
|
+
/**
|
|
179
|
+
* Search user tasks through the read model (mirrors `POST
|
|
180
|
+
* /user-tasks/search`). Returns `null` until the engine is ready. Honours an
|
|
181
|
+
* optional `{ state? }` filter, e.g. `searchUserTasks('{"state":"CREATED"}')`.
|
|
182
|
+
*/
|
|
183
|
+
searchUserTasks(filterJson?: string): UserTaskSearchQueryResult | null;
|
|
184
|
+
/**
|
|
185
|
+
* Search process instances through the read model (mirrors `POST
|
|
186
|
+
* /process-instances/search`). Returns `null` until the engine is ready.
|
|
187
|
+
*/
|
|
188
|
+
searchProcessInstances(filterJson?: string): ProcessInstanceSearchQueryResult | null;
|
|
189
|
+
/**
|
|
190
|
+
* Search variables through the read model (mirrors `POST
|
|
191
|
+
* /variables/search`). Returns `null` until the engine is ready.
|
|
192
|
+
*/
|
|
193
|
+
searchVariables(filterJson?: string): VariableSearchQueryResult | null;
|
|
194
|
+
/**
|
|
195
|
+
* The latest deployed form for `formKey` (mirrors `GET /forms/{formKey}`), or
|
|
196
|
+
* `null` if none exists — also `null` until the engine is ready.
|
|
197
|
+
*/
|
|
198
|
+
getFormByKey(formKey: string): FormResult | null;
|
|
199
|
+
/**
|
|
200
|
+
* The generic resource for `resourceKey` (mirrors `GET
|
|
201
|
+
* /resources/{resourceKey}`), or `null` if none exists — also `null` until the
|
|
202
|
+
* engine is ready.
|
|
203
|
+
*/
|
|
204
|
+
getResourceByKey(resourceKey: string): ResourceResult | null;
|
|
205
|
+
/**
|
|
206
|
+
* This session's engine read-model handle as `@nanobpm/engine-testkit`'s
|
|
207
|
+
* structural `EngineReadModel` port — feed it straight to `assertThatInstance`
|
|
208
|
+
* / `assertThatUserTask`. Returns `null` until the engine is ready (or on a
|
|
209
|
+
* lean-variant hook). Like the other read queries it is a pull projection: the
|
|
210
|
+
* handle reads live engine state on each call, so re-read it (keyed on
|
|
211
|
+
* {@link readModelVersion}) rather than caching its results.
|
|
212
|
+
*/
|
|
213
|
+
readModel(): EngineReadModel | null;
|
|
214
|
+
/**
|
|
215
|
+
* A monotonically increasing counter bumped after every command / worker round
|
|
216
|
+
* (and on deploy / reset). It is the reactivity signal for the pull read
|
|
217
|
+
* queries: key a `useMemo`/`useEffect` on it to re-run a query when the read
|
|
218
|
+
* model may have changed. {@link useReadModel} is the ready-made selector over
|
|
219
|
+
* it.
|
|
220
|
+
*/
|
|
221
|
+
readModelVersion: number;
|
|
222
|
+
}
|
|
135
223
|
/**
|
|
136
224
|
* React binding over a headless {@link BojtosSession}: owns the engine's
|
|
137
225
|
* lifecycle and the reactive `snapshot` / `events` / `processIds` state, and
|
|
@@ -142,5 +230,38 @@ export interface BojtosControls {
|
|
|
142
230
|
* This is the reactive half of the Bojtos public API (ADR 0043 §2); the console
|
|
143
231
|
* test-run panel is its first consumer (§8 step 2 — dogfooding is the acceptance
|
|
144
232
|
* test).
|
|
233
|
+
*
|
|
234
|
+
* With `variant: "readmodel"` the return type widens to
|
|
235
|
+
* {@link ReadModelBojtosControls}, adding the read channel + `readModelVersion`;
|
|
236
|
+
* the default `"lean"` variant returns the plain {@link BojtosControls} and never
|
|
237
|
+
* downloads the heavier read-model binary.
|
|
238
|
+
*/
|
|
239
|
+
export declare function useBojtos(options: UseBojtosOptions & {
|
|
240
|
+
variant: "readmodel";
|
|
241
|
+
}): ReadModelBojtosControls;
|
|
242
|
+
export declare function useBojtos(options: UseBojtosOptions & {
|
|
243
|
+
variant?: "lean";
|
|
244
|
+
}): BojtosControls;
|
|
245
|
+
export declare function useBojtos(options: UseBojtosOptions): BojtosControls | ReadModelBojtosControls;
|
|
246
|
+
/**
|
|
247
|
+
* Reactively project a value out of a `readmodel` hook's read channel, re-run
|
|
248
|
+
* whenever the read model may have moved.
|
|
249
|
+
*
|
|
250
|
+
* The read queries are pull projections (see {@link ReadModelBojtosControls}),
|
|
251
|
+
* so this is the ready-made "selector" that lands their result in React state on
|
|
252
|
+
* your terms: pass the `readmodel` {@link useBojtos} controls and a `select`
|
|
253
|
+
* that calls whichever read methods you care about (with whatever filters), and
|
|
254
|
+
* the memoized result re-computes each time `readModelVersion` bumps — i.e.
|
|
255
|
+
* after every command / worker round / deploy / reset — or the load `phase`
|
|
256
|
+
* flips. Before the engine is ready the read methods return `null`, so a
|
|
257
|
+
* selector must tolerate nulls.
|
|
258
|
+
*
|
|
259
|
+
* ```tsx
|
|
260
|
+
* const run = useBojtos({ bpmn, variant: "readmodel" });
|
|
261
|
+
* const openTasks = useReadModel(
|
|
262
|
+
* run,
|
|
263
|
+
* (rm) => rm.searchUserTasks('{"state":"CREATED"}')?.items ?? [],
|
|
264
|
+
* );
|
|
265
|
+
* ```
|
|
145
266
|
*/
|
|
146
|
-
export declare function
|
|
267
|
+
export declare function useReadModel<T>(controls: ReadModelBojtosControls, select: (controls: ReadModelBojtosControls) => T): T;
|
package/dist/useBojtos.js
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { createBojtosSession, dispatchRound, dispatchWorkers, } from "@nanobpm/bojtos-kit";
|
|
3
|
-
import { bpmnKey, capEvents, resourceList } from "./runState.js";
|
|
4
|
-
|
|
5
|
-
* React binding over a headless {@link BojtosSession}: owns the engine's
|
|
6
|
-
* lifecycle and the reactive `snapshot` / `events` / `processIds` state, and
|
|
7
|
-
* exposes the engine commands. The consuming component owns its own form state
|
|
8
|
-
* (selected process, seed vars, per-job output) and drives the visual contract
|
|
9
|
-
* (`<BpmnRuntimeView>` + the variable payload) off `snapshot`.
|
|
10
|
-
*
|
|
11
|
-
* This is the reactive half of the Bojtos public API (ADR 0043 §2); the console
|
|
12
|
-
* test-run panel is its first consumer (§8 step 2 — dogfooding is the acceptance
|
|
13
|
-
* test).
|
|
14
|
-
*/
|
|
15
|
-
export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
3
|
+
import { bpmnKey, capEvents, resolveVariant, resourceList, selectReadModel, } from "./runState.js";
|
|
4
|
+
export function useBojtos({ bpmn, wasm, maxEvents, variant, }) {
|
|
16
5
|
const sessionRef = useRef(null);
|
|
6
|
+
// The same session, narrowed to its read channel, but only when we actually
|
|
7
|
+
// asked for the `readmodel` variant. Kept as its own ref (rather than casting
|
|
8
|
+
// `sessionRef`) so the read methods reach the query surface without a cast — a
|
|
9
|
+
// lean session simply leaves this null and every read pull returns null.
|
|
10
|
+
const readModelRef = useRef(null);
|
|
17
11
|
const [phase, setPhase] = useState("loading");
|
|
18
12
|
const [error, setError] = useState(null);
|
|
19
13
|
const [processIds, setProcessIds] = useState([]);
|
|
20
14
|
const [snapshot, setSnapshot] = useState(null);
|
|
21
15
|
const [events, setEvents] = useState([]);
|
|
16
|
+
// Bumped whenever the read model may have moved (any command / round / deploy /
|
|
17
|
+
// reset) so pull read queries can be made reactive by keying on it.
|
|
18
|
+
const [readModelVersion, setReadModelVersion] = useState(0);
|
|
19
|
+
const bumpReadModel = useCallback(() => setReadModelVersion((v) => v + 1), []);
|
|
20
|
+
// The variant is an init-time concern like `wasm` (read when a session is
|
|
21
|
+
// first created for a diagram), so keep it in a ref rather than the deploy
|
|
22
|
+
// effect's deps.
|
|
23
|
+
const variantRef = useRef(variant);
|
|
24
|
+
variantRef.current = variant;
|
|
22
25
|
// The wasm source is an init-time concern (the first `ensureWasm` wins), so
|
|
23
26
|
// keep it in a ref rather than the mount effect's deps — a fresh URL/bytes
|
|
24
27
|
// identity each render must not re-create the session.
|
|
@@ -47,6 +50,9 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
47
50
|
setSnapshot(null);
|
|
48
51
|
setEvents([]);
|
|
49
52
|
setError(null);
|
|
53
|
+
// The read model was just wiped and re-seeded by the redeploy, so any
|
|
54
|
+
// reactive read query must re-run.
|
|
55
|
+
bumpReadModel();
|
|
50
56
|
},
|
|
51
57
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
58
|
[deployKey]);
|
|
@@ -61,7 +67,14 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
61
67
|
setSnapshot(null);
|
|
62
68
|
setEvents([]);
|
|
63
69
|
setError(null);
|
|
64
|
-
|
|
70
|
+
// Resolve the session with the requested variant. The `readmodel` branch
|
|
71
|
+
// keeps the narrowed `ReadModelBojtosSession` so the read methods reach the
|
|
72
|
+
// query surface without a cast; the lean branch leaves `readModelRef` null.
|
|
73
|
+
const variant = resolveVariant(variantRef.current);
|
|
74
|
+
const pending = variant === "readmodel"
|
|
75
|
+
? createBojtosSession({ wasm: wasmRef.current, variant: "readmodel" })
|
|
76
|
+
: createBojtosSession({ wasm: wasmRef.current });
|
|
77
|
+
pending
|
|
65
78
|
.then((session) => {
|
|
66
79
|
if (cancelled) {
|
|
67
80
|
session.free();
|
|
@@ -80,6 +93,8 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
80
93
|
return;
|
|
81
94
|
}
|
|
82
95
|
sessionRef.current = session;
|
|
96
|
+
readModelRef.current =
|
|
97
|
+
variant === "readmodel" ? session : null;
|
|
83
98
|
setPhase("ready");
|
|
84
99
|
})
|
|
85
100
|
.catch((e) => {
|
|
@@ -92,6 +107,7 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
92
107
|
cancelled = true;
|
|
93
108
|
sessionRef.current?.free();
|
|
94
109
|
sessionRef.current = null;
|
|
110
|
+
readModelRef.current = null;
|
|
95
111
|
};
|
|
96
112
|
}, [deployInto]);
|
|
97
113
|
const run = useCallback((fn) => {
|
|
@@ -103,13 +119,15 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
103
119
|
setSnapshot(snap);
|
|
104
120
|
setEvents(readEvents(session));
|
|
105
121
|
setError(null);
|
|
122
|
+
// A command may have moved the read model; signal reactive readers.
|
|
123
|
+
bumpReadModel();
|
|
106
124
|
return snap;
|
|
107
125
|
}
|
|
108
126
|
catch (e) {
|
|
109
127
|
setError(String(e));
|
|
110
128
|
return null;
|
|
111
129
|
}
|
|
112
|
-
}, []);
|
|
130
|
+
}, [bumpReadModel, readEvents]);
|
|
113
131
|
const createInstance = useCallback((processId, variablesJson) => run((s) => s.createInstance(processId, variablesJson)), [run]);
|
|
114
132
|
const completeJob = useCallback((jobKey, variablesJson) => run((s) => s.completeJob(jobKey, variablesJson)), [run]);
|
|
115
133
|
const completeAgentJob = useCallback((jobKey, result) => run((s) => s.completeAgentJob(jobKey, result)), [run]);
|
|
@@ -140,6 +158,7 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
140
158
|
setSnapshot(settled);
|
|
141
159
|
setEvents(readEvents(session));
|
|
142
160
|
setError(null);
|
|
161
|
+
bumpReadModel();
|
|
143
162
|
return settled;
|
|
144
163
|
}
|
|
145
164
|
catch (e) {
|
|
@@ -150,9 +169,10 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
150
169
|
setSnapshot(session.snapshot());
|
|
151
170
|
setEvents(readEvents(session));
|
|
152
171
|
setError(String(e));
|
|
172
|
+
bumpReadModel();
|
|
153
173
|
return null;
|
|
154
174
|
}
|
|
155
|
-
}, []);
|
|
175
|
+
}, [bumpReadModel, readEvents]);
|
|
156
176
|
const stepWorkers = useCallback(async (workers, opts) => {
|
|
157
177
|
const session = sessionRef.current;
|
|
158
178
|
if (!session)
|
|
@@ -165,6 +185,7 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
165
185
|
setSnapshot(round.snapshot);
|
|
166
186
|
setEvents(readEvents(session));
|
|
167
187
|
setError(null);
|
|
188
|
+
bumpReadModel();
|
|
168
189
|
return round;
|
|
169
190
|
}
|
|
170
191
|
catch (e) {
|
|
@@ -173,9 +194,10 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
173
194
|
setSnapshot(session.snapshot());
|
|
174
195
|
setEvents(readEvents(session));
|
|
175
196
|
setError(String(e));
|
|
197
|
+
bumpReadModel();
|
|
176
198
|
return null;
|
|
177
199
|
}
|
|
178
|
-
}, []);
|
|
200
|
+
}, [bumpReadModel, readEvents]);
|
|
179
201
|
const reset = useCallback(() => {
|
|
180
202
|
const session = sessionRef.current;
|
|
181
203
|
if (!session)
|
|
@@ -191,6 +213,17 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
191
213
|
setError(String(e));
|
|
192
214
|
}
|
|
193
215
|
}, [deployInto]);
|
|
216
|
+
// The read channel. Each pull returns null when there is no live read-model
|
|
217
|
+
// session (loading, or a lean-variant hook), via the shared `selectReadModel`
|
|
218
|
+
// guard, rather than throwing on a missing engine. They intentionally do not
|
|
219
|
+
// touch React state themselves — reactivity is opt-in through
|
|
220
|
+
// `readModelVersion` / `useReadModel` (see `ReadModelBojtosControls`).
|
|
221
|
+
const searchUserTasks = useCallback((filterJson) => selectReadModel(readModelRef.current, (rm) => rm.searchUserTasks(filterJson)), []);
|
|
222
|
+
const searchProcessInstances = useCallback((filterJson) => selectReadModel(readModelRef.current, (rm) => rm.searchProcessInstances(filterJson)), []);
|
|
223
|
+
const searchVariables = useCallback((filterJson) => selectReadModel(readModelRef.current, (rm) => rm.searchVariables(filterJson)), []);
|
|
224
|
+
const getFormByKey = useCallback((formKey) => selectReadModel(readModelRef.current, (rm) => rm.getFormByKey(formKey)), []);
|
|
225
|
+
const getResourceByKey = useCallback((resourceKey) => selectReadModel(readModelRef.current, (rm) => rm.getResourceByKey(resourceKey)), []);
|
|
226
|
+
const readModel = useCallback(() => selectReadModel(readModelRef.current, (rm) => rm.readModel()), []);
|
|
194
227
|
return {
|
|
195
228
|
phase,
|
|
196
229
|
error,
|
|
@@ -217,5 +250,45 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
217
250
|
runWorkers,
|
|
218
251
|
stepWorkers,
|
|
219
252
|
reset,
|
|
253
|
+
searchUserTasks,
|
|
254
|
+
searchProcessInstances,
|
|
255
|
+
searchVariables,
|
|
256
|
+
getFormByKey,
|
|
257
|
+
getResourceByKey,
|
|
258
|
+
readModel,
|
|
259
|
+
readModelVersion,
|
|
220
260
|
};
|
|
221
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Reactively project a value out of a `readmodel` hook's read channel, re-run
|
|
264
|
+
* whenever the read model may have moved.
|
|
265
|
+
*
|
|
266
|
+
* The read queries are pull projections (see {@link ReadModelBojtosControls}),
|
|
267
|
+
* so this is the ready-made "selector" that lands their result in React state on
|
|
268
|
+
* your terms: pass the `readmodel` {@link useBojtos} controls and a `select`
|
|
269
|
+
* that calls whichever read methods you care about (with whatever filters), and
|
|
270
|
+
* the memoized result re-computes each time `readModelVersion` bumps — i.e.
|
|
271
|
+
* after every command / worker round / deploy / reset — or the load `phase`
|
|
272
|
+
* flips. Before the engine is ready the read methods return `null`, so a
|
|
273
|
+
* selector must tolerate nulls.
|
|
274
|
+
*
|
|
275
|
+
* ```tsx
|
|
276
|
+
* const run = useBojtos({ bpmn, variant: "readmodel" });
|
|
277
|
+
* const openTasks = useReadModel(
|
|
278
|
+
* run,
|
|
279
|
+
* (rm) => rm.searchUserTasks('{"state":"CREATED"}')?.items ?? [],
|
|
280
|
+
* );
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
export function useReadModel(controls, select) {
|
|
284
|
+
// Keep the latest selector and controls without making them memo dependencies:
|
|
285
|
+
// re-running is driven by the read model moving (`readModelVersion`) / readiness
|
|
286
|
+
// (`phase`), not by a fresh inline selector or a fresh `controls` object literal
|
|
287
|
+
// (`useBojtos` returns a new object each render, so depending on it directly would
|
|
288
|
+
// re-run the selector on *every* parent re-render).
|
|
289
|
+
const selectRef = useRef(select);
|
|
290
|
+
selectRef.current = select;
|
|
291
|
+
const controlsRef = useRef(controls);
|
|
292
|
+
controlsRef.current = controls;
|
|
293
|
+
return useMemo(() => selectRef.current(controlsRef.current), [controls.readModelVersion, controls.phase]);
|
|
294
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/bojtos-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
"src"
|
|
25
25
|
],
|
|
26
26
|
"sideEffects": false,
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22.6"
|
|
29
|
+
},
|
|
27
30
|
"scripts": {
|
|
28
31
|
"build": "tsc -p tsconfig.json",
|
|
29
32
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
@@ -32,7 +35,7 @@
|
|
|
32
35
|
"test:ci": "node --experimental-strip-types --test test/*.test.ts"
|
|
33
36
|
},
|
|
34
37
|
"dependencies": {
|
|
35
|
-
"@nanobpm/bojtos-kit": "^0.
|
|
38
|
+
"@nanobpm/bojtos-kit": "^0.9.0"
|
|
36
39
|
},
|
|
37
40
|
"peerDependencies": {
|
|
38
41
|
"bpmn-js": ">=17",
|
package/src/index.ts
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
export {
|
|
8
8
|
useBojtos,
|
|
9
|
+
useReadModel,
|
|
9
10
|
type UseBojtosOptions,
|
|
10
11
|
type BojtosControls,
|
|
12
|
+
type ReadModelBojtosControls,
|
|
11
13
|
type BojtosPhase,
|
|
12
14
|
} from "./useBojtos.js";
|
|
13
15
|
export { Bojtos, type BojtosProps, type TraceEvent } from "./Bojtos.js";
|
|
@@ -29,6 +31,8 @@ export {
|
|
|
29
31
|
bpmnKey,
|
|
30
32
|
resourceList,
|
|
31
33
|
capEvents,
|
|
34
|
+
resolveVariant,
|
|
35
|
+
selectReadModel,
|
|
32
36
|
} from "./runState.js";
|
|
33
37
|
export {
|
|
34
38
|
JobFailure,
|
|
@@ -48,6 +52,8 @@ export {
|
|
|
48
52
|
} from "@nanobpm/bojtos-kit";
|
|
49
53
|
export type {
|
|
50
54
|
BojtosSession,
|
|
55
|
+
ReadModelBojtosSession,
|
|
56
|
+
EngineVariant,
|
|
51
57
|
Snapshot,
|
|
52
58
|
InstanceDto,
|
|
53
59
|
JobDto,
|
|
@@ -71,4 +77,14 @@ export type {
|
|
|
71
77
|
TraceTurnGroup,
|
|
72
78
|
TraceItem,
|
|
73
79
|
TraceAdapter,
|
|
80
|
+
UserTaskSearchQueryResult,
|
|
81
|
+
UserTaskResult,
|
|
82
|
+
ProcessInstanceSearchQueryResult,
|
|
83
|
+
ProcessInstanceResult,
|
|
84
|
+
VariableSearchQueryResult,
|
|
85
|
+
VariableResult,
|
|
86
|
+
FormResult,
|
|
87
|
+
ResourceResult,
|
|
88
|
+
SearchQueryResponse,
|
|
89
|
+
SearchQueryPageResponse,
|
|
74
90
|
} from "@nanobpm/bojtos-kit";
|
package/src/runState.ts
CHANGED
|
@@ -8,6 +8,36 @@
|
|
|
8
8
|
* decisions, and decisions are worth testing.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type { EngineVariant } from "@nanobpm/bojtos-kit";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The engine variant a hook run should use, defaulting to `"lean"` when the
|
|
15
|
+
* consumer didn't pick one. Keeping the default here (rather than a parameter
|
|
16
|
+
* default) makes the "no `variant` means lean, so existing consumers are
|
|
17
|
+
* unaffected" decision a single testable fact instead of an inline `?? "lean"`
|
|
18
|
+
* scattered through the hook.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveVariant(
|
|
21
|
+
variant: EngineVariant | undefined,
|
|
22
|
+
): EngineVariant {
|
|
23
|
+
return variant ?? "lean";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pull a value out of the read-model channel, or `null` when there is no live
|
|
28
|
+
* read-model session (loading, a lean-variant hook, or between a teardown and
|
|
29
|
+
* the next engine). This is the one place the "no session → null, otherwise run
|
|
30
|
+
* the query" decision lives, shared by every reactive read method so a
|
|
31
|
+
* lean-variant call or a mid-load call is a quiet `null` rather than a throw on
|
|
32
|
+
* a missing engine.
|
|
33
|
+
*/
|
|
34
|
+
export function selectReadModel<C, T>(
|
|
35
|
+
channel: C | null,
|
|
36
|
+
select: (channel: C) => T,
|
|
37
|
+
): T | null {
|
|
38
|
+
return channel === null ? null : select(channel);
|
|
39
|
+
}
|
|
40
|
+
|
|
11
41
|
/**
|
|
12
42
|
* Stable key for a marker set, so unchanged ids don't re-paint the diagram.
|
|
13
43
|
*
|
package/src/useBojtos.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
type ActivateInstruction,
|
|
4
4
|
type AgentResult,
|
|
@@ -7,13 +7,27 @@ import {
|
|
|
7
7
|
type DispatchOptions,
|
|
8
8
|
dispatchRound,
|
|
9
9
|
dispatchWorkers,
|
|
10
|
+
type EngineReadModel,
|
|
11
|
+
type EngineVariant,
|
|
12
|
+
type FormResult,
|
|
10
13
|
type JobHandler,
|
|
14
|
+
type ProcessInstanceSearchQueryResult,
|
|
15
|
+
type ReadModelBojtosSession,
|
|
16
|
+
type ResourceResult,
|
|
11
17
|
type RoundResult,
|
|
12
18
|
type Snapshot,
|
|
19
|
+
type UserTaskSearchQueryResult,
|
|
20
|
+
type VariableSearchQueryResult,
|
|
13
21
|
type WasmEvent,
|
|
14
22
|
type WasmSource,
|
|
15
23
|
} from "@nanobpm/bojtos-kit";
|
|
16
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
bpmnKey,
|
|
26
|
+
capEvents,
|
|
27
|
+
resolveVariant,
|
|
28
|
+
resourceList,
|
|
29
|
+
selectReadModel,
|
|
30
|
+
} from "./runState.js";
|
|
17
31
|
|
|
18
32
|
/** Lifecycle of the in-browser engine load. */
|
|
19
33
|
export type BojtosPhase = "loading" | "ready" | "error";
|
|
@@ -46,6 +60,22 @@ export interface UseBojtosOptions {
|
|
|
46
60
|
* whole log, which stays the default so existing consumers are unaffected.
|
|
47
61
|
*/
|
|
48
62
|
maxEvents?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Which engine variant to load, defaulting to `"lean"` — existing consumers
|
|
65
|
+
* are unaffected. Pass `"readmodel"` to also thread the gateway's
|
|
66
|
+
* Camunda-parity REST read channel through the hook: the returned controls
|
|
67
|
+
* then widen to {@link ReadModelBojtosControls}, exposing `searchUserTasks` /
|
|
68
|
+
* `searchProcessInstances` / `searchVariables` / `getFormByKey` /
|
|
69
|
+
* `getResourceByKey` plus the `readModelVersion` reactivity signal.
|
|
70
|
+
*
|
|
71
|
+
* Init-time only, like `wasm`: the variant is read when the session is first
|
|
72
|
+
* created for a given diagram, so changing it later has no effect until the
|
|
73
|
+
* next `bpmn` change re-creates the engine.
|
|
74
|
+
*
|
|
75
|
+
* The read-model binary is heavier and only code-splits in when this is
|
|
76
|
+
* `"readmodel"` (ADR 0043 §3); a lean hook never downloads it.
|
|
77
|
+
*/
|
|
78
|
+
variant?: EngineVariant;
|
|
49
79
|
}
|
|
50
80
|
|
|
51
81
|
export interface BojtosControls {
|
|
@@ -177,6 +207,81 @@ export interface BojtosControls {
|
|
|
177
207
|
reset(): void;
|
|
178
208
|
}
|
|
179
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The {@link BojtosControls} of a `readmodel`-variant hook: the full command
|
|
212
|
+
* surface **plus** reactive access to the gateway's Camunda-parity REST read
|
|
213
|
+
* channel. You get one by passing `variant: "readmodel"` to {@link useBojtos},
|
|
214
|
+
* which widens the return type from `BojtosControls` to this.
|
|
215
|
+
*
|
|
216
|
+
* ## Reactivity model
|
|
217
|
+
*
|
|
218
|
+
* The read queries are **pull** projections of the read model, not part of the
|
|
219
|
+
* command→`snapshot` push loop: `searchUserTasks` et al. answer "what does the
|
|
220
|
+
* read model say *right now*", and there is no single obvious cadence at which
|
|
221
|
+
* to re-run them (a consumer may care about tasks, another about variables, each
|
|
222
|
+
* with its own filter). So rather than eagerly re-running every query after
|
|
223
|
+
* every command and stuffing five results into state, the hook exposes:
|
|
224
|
+
*
|
|
225
|
+
* - the five read methods as **imperative pulls** — call one whenever you want a
|
|
226
|
+
* fresh answer; each returns `null` before the engine is ready rather than
|
|
227
|
+
* throwing, and
|
|
228
|
+
* - {@link readModelVersion}, a counter bumped after **every** command / worker
|
|
229
|
+
* round (i.e. whenever the read model may have moved), so a consumer can make
|
|
230
|
+
* a query reactive by keying a `useMemo`/`useEffect` on it — or just let
|
|
231
|
+
* {@link useReadModel} do exactly that.
|
|
232
|
+
*
|
|
233
|
+
* This keeps the read channel opt-in and filter-agnostic while still landing its
|
|
234
|
+
* results in React state on the consumer's terms.
|
|
235
|
+
*/
|
|
236
|
+
export interface ReadModelBojtosControls extends BojtosControls {
|
|
237
|
+
/**
|
|
238
|
+
* Search user tasks through the read model (mirrors `POST
|
|
239
|
+
* /user-tasks/search`). Returns `null` until the engine is ready. Honours an
|
|
240
|
+
* optional `{ state? }` filter, e.g. `searchUserTasks('{"state":"CREATED"}')`.
|
|
241
|
+
*/
|
|
242
|
+
searchUserTasks(filterJson?: string): UserTaskSearchQueryResult | null;
|
|
243
|
+
/**
|
|
244
|
+
* Search process instances through the read model (mirrors `POST
|
|
245
|
+
* /process-instances/search`). Returns `null` until the engine is ready.
|
|
246
|
+
*/
|
|
247
|
+
searchProcessInstances(
|
|
248
|
+
filterJson?: string,
|
|
249
|
+
): ProcessInstanceSearchQueryResult | null;
|
|
250
|
+
/**
|
|
251
|
+
* Search variables through the read model (mirrors `POST
|
|
252
|
+
* /variables/search`). Returns `null` until the engine is ready.
|
|
253
|
+
*/
|
|
254
|
+
searchVariables(filterJson?: string): VariableSearchQueryResult | null;
|
|
255
|
+
/**
|
|
256
|
+
* The latest deployed form for `formKey` (mirrors `GET /forms/{formKey}`), or
|
|
257
|
+
* `null` if none exists — also `null` until the engine is ready.
|
|
258
|
+
*/
|
|
259
|
+
getFormByKey(formKey: string): FormResult | null;
|
|
260
|
+
/**
|
|
261
|
+
* The generic resource for `resourceKey` (mirrors `GET
|
|
262
|
+
* /resources/{resourceKey}`), or `null` if none exists — also `null` until the
|
|
263
|
+
* engine is ready.
|
|
264
|
+
*/
|
|
265
|
+
getResourceByKey(resourceKey: string): ResourceResult | null;
|
|
266
|
+
/**
|
|
267
|
+
* This session's engine read-model handle as `@nanobpm/engine-testkit`'s
|
|
268
|
+
* structural `EngineReadModel` port — feed it straight to `assertThatInstance`
|
|
269
|
+
* / `assertThatUserTask`. Returns `null` until the engine is ready (or on a
|
|
270
|
+
* lean-variant hook). Like the other read queries it is a pull projection: the
|
|
271
|
+
* handle reads live engine state on each call, so re-read it (keyed on
|
|
272
|
+
* {@link readModelVersion}) rather than caching its results.
|
|
273
|
+
*/
|
|
274
|
+
readModel(): EngineReadModel | null;
|
|
275
|
+
/**
|
|
276
|
+
* A monotonically increasing counter bumped after every command / worker round
|
|
277
|
+
* (and on deploy / reset). It is the reactivity signal for the pull read
|
|
278
|
+
* queries: key a `useMemo`/`useEffect` on it to re-run a query when the read
|
|
279
|
+
* model may have changed. {@link useReadModel} is the ready-made selector over
|
|
280
|
+
* it.
|
|
281
|
+
*/
|
|
282
|
+
readModelVersion: number;
|
|
283
|
+
}
|
|
284
|
+
|
|
180
285
|
/**
|
|
181
286
|
* Session members the hook deliberately does not re-export: the deployment
|
|
182
287
|
* lifecycle it owns itself, and the low-level activate primitive the dispatch
|
|
@@ -200,6 +305,20 @@ type UnboundCommands = Exclude<
|
|
|
200
305
|
type AssertNever<T extends never> = T;
|
|
201
306
|
type _EverySessionCommandIsBound = AssertNever<UnboundCommands>;
|
|
202
307
|
|
|
308
|
+
/**
|
|
309
|
+
* The same guard for the widened `readmodel` surface: every method of a
|
|
310
|
+
* {@link ReadModelBojtosSession} — the lean commands **and** the five read
|
|
311
|
+
* queries — must be bound on {@link ReadModelBojtosControls}, or a `readmodel`
|
|
312
|
+
* hook would silently drop part of the read channel (the exact failure mode #1
|
|
313
|
+
* described, now covering the read methods too). Adding a read query to the
|
|
314
|
+
* session without binding it here fails the build with its name.
|
|
315
|
+
*/
|
|
316
|
+
type UnboundReadModelCommands = Exclude<
|
|
317
|
+
keyof ReadModelBojtosSession,
|
|
318
|
+
NotReExported | keyof ReadModelBojtosControls
|
|
319
|
+
>;
|
|
320
|
+
type _EveryReadModelCommandIsBound = AssertNever<UnboundReadModelCommands>;
|
|
321
|
+
|
|
203
322
|
/**
|
|
204
323
|
* React binding over a headless {@link BojtosSession}: owns the engine's
|
|
205
324
|
* lifecycle and the reactive `snapshot` / `events` / `processIds` state, and
|
|
@@ -210,18 +329,51 @@ type _EverySessionCommandIsBound = AssertNever<UnboundCommands>;
|
|
|
210
329
|
* This is the reactive half of the Bojtos public API (ADR 0043 §2); the console
|
|
211
330
|
* test-run panel is its first consumer (§8 step 2 — dogfooding is the acceptance
|
|
212
331
|
* test).
|
|
332
|
+
*
|
|
333
|
+
* With `variant: "readmodel"` the return type widens to
|
|
334
|
+
* {@link ReadModelBojtosControls}, adding the read channel + `readModelVersion`;
|
|
335
|
+
* the default `"lean"` variant returns the plain {@link BojtosControls} and never
|
|
336
|
+
* downloads the heavier read-model binary.
|
|
213
337
|
*/
|
|
338
|
+
export function useBojtos(
|
|
339
|
+
options: UseBojtosOptions & { variant: "readmodel" },
|
|
340
|
+
): ReadModelBojtosControls;
|
|
341
|
+
export function useBojtos(
|
|
342
|
+
options: UseBojtosOptions & { variant?: "lean" },
|
|
343
|
+
): BojtosControls;
|
|
344
|
+
export function useBojtos(
|
|
345
|
+
options: UseBojtosOptions,
|
|
346
|
+
): BojtosControls | ReadModelBojtosControls;
|
|
214
347
|
export function useBojtos({
|
|
215
348
|
bpmn,
|
|
216
349
|
wasm,
|
|
217
350
|
maxEvents,
|
|
218
|
-
|
|
351
|
+
variant,
|
|
352
|
+
}: UseBojtosOptions): ReadModelBojtosControls {
|
|
219
353
|
const sessionRef = useRef<BojtosSession | null>(null);
|
|
354
|
+
// The same session, narrowed to its read channel, but only when we actually
|
|
355
|
+
// asked for the `readmodel` variant. Kept as its own ref (rather than casting
|
|
356
|
+
// `sessionRef`) so the read methods reach the query surface without a cast — a
|
|
357
|
+
// lean session simply leaves this null and every read pull returns null.
|
|
358
|
+
const readModelRef = useRef<ReadModelBojtosSession | null>(null);
|
|
220
359
|
const [phase, setPhase] = useState<BojtosPhase>("loading");
|
|
221
360
|
const [error, setError] = useState<string | null>(null);
|
|
222
361
|
const [processIds, setProcessIds] = useState<string[]>([]);
|
|
223
362
|
const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
|
|
224
363
|
const [events, setEvents] = useState<WasmEvent[]>([]);
|
|
364
|
+
// Bumped whenever the read model may have moved (any command / round / deploy /
|
|
365
|
+
// reset) so pull read queries can be made reactive by keying on it.
|
|
366
|
+
const [readModelVersion, setReadModelVersion] = useState(0);
|
|
367
|
+
const bumpReadModel = useCallback(
|
|
368
|
+
() => setReadModelVersion((v) => v + 1),
|
|
369
|
+
[],
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// The variant is an init-time concern like `wasm` (read when a session is
|
|
373
|
+
// first created for a diagram), so keep it in a ref rather than the deploy
|
|
374
|
+
// effect's deps.
|
|
375
|
+
const variantRef = useRef(variant);
|
|
376
|
+
variantRef.current = variant;
|
|
225
377
|
|
|
226
378
|
// The wasm source is an init-time concern (the first `ensureWasm` wins), so
|
|
227
379
|
// keep it in a ref rather than the mount effect's deps — a fresh URL/bytes
|
|
@@ -254,6 +406,9 @@ export function useBojtos({
|
|
|
254
406
|
setSnapshot(null);
|
|
255
407
|
setEvents([]);
|
|
256
408
|
setError(null);
|
|
409
|
+
// The read model was just wiped and re-seeded by the redeploy, so any
|
|
410
|
+
// reactive read query must re-run.
|
|
411
|
+
bumpReadModel();
|
|
257
412
|
},
|
|
258
413
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
259
414
|
[deployKey],
|
|
@@ -270,7 +425,15 @@ export function useBojtos({
|
|
|
270
425
|
setSnapshot(null);
|
|
271
426
|
setEvents([]);
|
|
272
427
|
setError(null);
|
|
273
|
-
|
|
428
|
+
// Resolve the session with the requested variant. The `readmodel` branch
|
|
429
|
+
// keeps the narrowed `ReadModelBojtosSession` so the read methods reach the
|
|
430
|
+
// query surface without a cast; the lean branch leaves `readModelRef` null.
|
|
431
|
+
const variant = resolveVariant(variantRef.current);
|
|
432
|
+
const pending =
|
|
433
|
+
variant === "readmodel"
|
|
434
|
+
? createBojtosSession({ wasm: wasmRef.current, variant: "readmodel" })
|
|
435
|
+
: createBojtosSession({ wasm: wasmRef.current });
|
|
436
|
+
pending
|
|
274
437
|
.then((session) => {
|
|
275
438
|
if (cancelled) {
|
|
276
439
|
session.free();
|
|
@@ -288,6 +451,8 @@ export function useBojtos({
|
|
|
288
451
|
return;
|
|
289
452
|
}
|
|
290
453
|
sessionRef.current = session;
|
|
454
|
+
readModelRef.current =
|
|
455
|
+
variant === "readmodel" ? (session as ReadModelBojtosSession) : null;
|
|
291
456
|
setPhase("ready");
|
|
292
457
|
})
|
|
293
458
|
.catch((e) => {
|
|
@@ -299,6 +464,7 @@ export function useBojtos({
|
|
|
299
464
|
cancelled = true;
|
|
300
465
|
sessionRef.current?.free();
|
|
301
466
|
sessionRef.current = null;
|
|
467
|
+
readModelRef.current = null;
|
|
302
468
|
};
|
|
303
469
|
}, [deployInto]);
|
|
304
470
|
|
|
@@ -311,13 +477,15 @@ export function useBojtos({
|
|
|
311
477
|
setSnapshot(snap);
|
|
312
478
|
setEvents(readEvents(session));
|
|
313
479
|
setError(null);
|
|
480
|
+
// A command may have moved the read model; signal reactive readers.
|
|
481
|
+
bumpReadModel();
|
|
314
482
|
return snap;
|
|
315
483
|
} catch (e) {
|
|
316
484
|
setError(String(e));
|
|
317
485
|
return null;
|
|
318
486
|
}
|
|
319
487
|
},
|
|
320
|
-
[],
|
|
488
|
+
[bumpReadModel, readEvents],
|
|
321
489
|
);
|
|
322
490
|
|
|
323
491
|
const createInstance = useCallback(
|
|
@@ -431,6 +599,7 @@ export function useBojtos({
|
|
|
431
599
|
setSnapshot(settled);
|
|
432
600
|
setEvents(readEvents(session));
|
|
433
601
|
setError(null);
|
|
602
|
+
bumpReadModel();
|
|
434
603
|
return settled;
|
|
435
604
|
} catch (e) {
|
|
436
605
|
if (sessionRef.current !== session) return null;
|
|
@@ -439,10 +608,11 @@ export function useBojtos({
|
|
|
439
608
|
setSnapshot(session.snapshot());
|
|
440
609
|
setEvents(readEvents(session));
|
|
441
610
|
setError(String(e));
|
|
611
|
+
bumpReadModel();
|
|
442
612
|
return null;
|
|
443
613
|
}
|
|
444
614
|
},
|
|
445
|
-
[],
|
|
615
|
+
[bumpReadModel, readEvents],
|
|
446
616
|
);
|
|
447
617
|
|
|
448
618
|
const stepWorkers = useCallback(
|
|
@@ -459,16 +629,18 @@ export function useBojtos({
|
|
|
459
629
|
setSnapshot(round.snapshot);
|
|
460
630
|
setEvents(readEvents(session));
|
|
461
631
|
setError(null);
|
|
632
|
+
bumpReadModel();
|
|
462
633
|
return round;
|
|
463
634
|
} catch (e) {
|
|
464
635
|
if (sessionRef.current !== session) return null;
|
|
465
636
|
setSnapshot(session.snapshot());
|
|
466
637
|
setEvents(readEvents(session));
|
|
467
638
|
setError(String(e));
|
|
639
|
+
bumpReadModel();
|
|
468
640
|
return null;
|
|
469
641
|
}
|
|
470
642
|
},
|
|
471
|
-
[],
|
|
643
|
+
[bumpReadModel, readEvents],
|
|
472
644
|
);
|
|
473
645
|
|
|
474
646
|
const reset = useCallback(() => {
|
|
@@ -485,6 +657,49 @@ export function useBojtos({
|
|
|
485
657
|
}
|
|
486
658
|
}, [deployInto]);
|
|
487
659
|
|
|
660
|
+
// The read channel. Each pull returns null when there is no live read-model
|
|
661
|
+
// session (loading, or a lean-variant hook), via the shared `selectReadModel`
|
|
662
|
+
// guard, rather than throwing on a missing engine. They intentionally do not
|
|
663
|
+
// touch React state themselves — reactivity is opt-in through
|
|
664
|
+
// `readModelVersion` / `useReadModel` (see `ReadModelBojtosControls`).
|
|
665
|
+
const searchUserTasks = useCallback(
|
|
666
|
+
(filterJson?: string) =>
|
|
667
|
+
selectReadModel(readModelRef.current, (rm) =>
|
|
668
|
+
rm.searchUserTasks(filterJson),
|
|
669
|
+
),
|
|
670
|
+
[],
|
|
671
|
+
);
|
|
672
|
+
const searchProcessInstances = useCallback(
|
|
673
|
+
(filterJson?: string) =>
|
|
674
|
+
selectReadModel(readModelRef.current, (rm) =>
|
|
675
|
+
rm.searchProcessInstances(filterJson),
|
|
676
|
+
),
|
|
677
|
+
[],
|
|
678
|
+
);
|
|
679
|
+
const searchVariables = useCallback(
|
|
680
|
+
(filterJson?: string) =>
|
|
681
|
+
selectReadModel(readModelRef.current, (rm) =>
|
|
682
|
+
rm.searchVariables(filterJson),
|
|
683
|
+
),
|
|
684
|
+
[],
|
|
685
|
+
);
|
|
686
|
+
const getFormByKey = useCallback(
|
|
687
|
+
(formKey: string) =>
|
|
688
|
+
selectReadModel(readModelRef.current, (rm) => rm.getFormByKey(formKey)),
|
|
689
|
+
[],
|
|
690
|
+
);
|
|
691
|
+
const getResourceByKey = useCallback(
|
|
692
|
+
(resourceKey: string) =>
|
|
693
|
+
selectReadModel(readModelRef.current, (rm) =>
|
|
694
|
+
rm.getResourceByKey(resourceKey),
|
|
695
|
+
),
|
|
696
|
+
[],
|
|
697
|
+
);
|
|
698
|
+
const readModel = useCallback(
|
|
699
|
+
() => selectReadModel(readModelRef.current, (rm) => rm.readModel()),
|
|
700
|
+
[],
|
|
701
|
+
);
|
|
702
|
+
|
|
488
703
|
return {
|
|
489
704
|
phase,
|
|
490
705
|
error,
|
|
@@ -511,5 +726,52 @@ export function useBojtos({
|
|
|
511
726
|
runWorkers,
|
|
512
727
|
stepWorkers,
|
|
513
728
|
reset,
|
|
729
|
+
searchUserTasks,
|
|
730
|
+
searchProcessInstances,
|
|
731
|
+
searchVariables,
|
|
732
|
+
getFormByKey,
|
|
733
|
+
getResourceByKey,
|
|
734
|
+
readModel,
|
|
735
|
+
readModelVersion,
|
|
514
736
|
};
|
|
515
737
|
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Reactively project a value out of a `readmodel` hook's read channel, re-run
|
|
741
|
+
* whenever the read model may have moved.
|
|
742
|
+
*
|
|
743
|
+
* The read queries are pull projections (see {@link ReadModelBojtosControls}),
|
|
744
|
+
* so this is the ready-made "selector" that lands their result in React state on
|
|
745
|
+
* your terms: pass the `readmodel` {@link useBojtos} controls and a `select`
|
|
746
|
+
* that calls whichever read methods you care about (with whatever filters), and
|
|
747
|
+
* the memoized result re-computes each time `readModelVersion` bumps — i.e.
|
|
748
|
+
* after every command / worker round / deploy / reset — or the load `phase`
|
|
749
|
+
* flips. Before the engine is ready the read methods return `null`, so a
|
|
750
|
+
* selector must tolerate nulls.
|
|
751
|
+
*
|
|
752
|
+
* ```tsx
|
|
753
|
+
* const run = useBojtos({ bpmn, variant: "readmodel" });
|
|
754
|
+
* const openTasks = useReadModel(
|
|
755
|
+
* run,
|
|
756
|
+
* (rm) => rm.searchUserTasks('{"state":"CREATED"}')?.items ?? [],
|
|
757
|
+
* );
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
export function useReadModel<T>(
|
|
761
|
+
controls: ReadModelBojtosControls,
|
|
762
|
+
select: (controls: ReadModelBojtosControls) => T,
|
|
763
|
+
): T {
|
|
764
|
+
// Keep the latest selector and controls without making them memo dependencies:
|
|
765
|
+
// re-running is driven by the read model moving (`readModelVersion`) / readiness
|
|
766
|
+
// (`phase`), not by a fresh inline selector or a fresh `controls` object literal
|
|
767
|
+
// (`useBojtos` returns a new object each render, so depending on it directly would
|
|
768
|
+
// re-run the selector on *every* parent re-render).
|
|
769
|
+
const selectRef = useRef(select);
|
|
770
|
+
selectRef.current = select;
|
|
771
|
+
const controlsRef = useRef(controls);
|
|
772
|
+
controlsRef.current = controls;
|
|
773
|
+
return useMemo(
|
|
774
|
+
() => selectRef.current(controlsRef.current),
|
|
775
|
+
[controls.readModelVersion, controls.phase],
|
|
776
|
+
);
|
|
777
|
+
}
|