@nanobpm/bojtos-react 0.7.0 → 0.8.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 +114 -2
- package/dist/useBojtos.js +89 -18
- package/package.json +2 -2
- package/src/index.ts +16 -0
- package/src/runState.ts +30 -0
- package/src/useBojtos.ts +254 -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 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,69 @@ 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
|
+
* A monotonically increasing counter bumped after every command / worker round
|
|
207
|
+
* (and on deploy / reset). It is the reactivity signal for the pull read
|
|
208
|
+
* queries: key a `useMemo`/`useEffect` on it to re-run a query when the read
|
|
209
|
+
* model may have changed. {@link useReadModel} is the ready-made selector over
|
|
210
|
+
* it.
|
|
211
|
+
*/
|
|
212
|
+
readModelVersion: number;
|
|
213
|
+
}
|
|
135
214
|
/**
|
|
136
215
|
* React binding over a headless {@link BojtosSession}: owns the engine's
|
|
137
216
|
* lifecycle and the reactive `snapshot` / `events` / `processIds` state, and
|
|
@@ -142,5 +221,38 @@ export interface BojtosControls {
|
|
|
142
221
|
* This is the reactive half of the Bojtos public API (ADR 0043 §2); the console
|
|
143
222
|
* test-run panel is its first consumer (§8 step 2 — dogfooding is the acceptance
|
|
144
223
|
* test).
|
|
224
|
+
*
|
|
225
|
+
* With `variant: "readmodel"` the return type widens to
|
|
226
|
+
* {@link ReadModelBojtosControls}, adding the read channel + `readModelVersion`;
|
|
227
|
+
* the default `"lean"` variant returns the plain {@link BojtosControls} and never
|
|
228
|
+
* downloads the heavier read-model binary.
|
|
229
|
+
*/
|
|
230
|
+
export declare function useBojtos(options: UseBojtosOptions & {
|
|
231
|
+
variant: "readmodel";
|
|
232
|
+
}): ReadModelBojtosControls;
|
|
233
|
+
export declare function useBojtos(options: UseBojtosOptions & {
|
|
234
|
+
variant?: "lean";
|
|
235
|
+
}): BojtosControls;
|
|
236
|
+
export declare function useBojtos(options: UseBojtosOptions): BojtosControls | ReadModelBojtosControls;
|
|
237
|
+
/**
|
|
238
|
+
* Reactively project a value out of a `readmodel` hook's read channel, re-run
|
|
239
|
+
* whenever the read model may have moved.
|
|
240
|
+
*
|
|
241
|
+
* The read queries are pull projections (see {@link ReadModelBojtosControls}),
|
|
242
|
+
* so this is the ready-made "selector" that lands their result in React state on
|
|
243
|
+
* your terms: pass the `readmodel` {@link useBojtos} controls and a `select`
|
|
244
|
+
* that calls whichever read methods you care about (with whatever filters), and
|
|
245
|
+
* the memoized result re-computes each time `readModelVersion` bumps — i.e.
|
|
246
|
+
* after every command / worker round / deploy / reset — or the load `phase`
|
|
247
|
+
* flips. Before the engine is ready the read methods return `null`, so a
|
|
248
|
+
* selector must tolerate nulls.
|
|
249
|
+
*
|
|
250
|
+
* ```tsx
|
|
251
|
+
* const run = useBojtos({ bpmn, variant: "readmodel" });
|
|
252
|
+
* const openTasks = useReadModel(
|
|
253
|
+
* run,
|
|
254
|
+
* (rm) => rm.searchUserTasks('{"state":"CREATED"}')?.items ?? [],
|
|
255
|
+
* );
|
|
256
|
+
* ```
|
|
145
257
|
*/
|
|
146
|
-
export declare function
|
|
258
|
+
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,16 @@ 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)), []);
|
|
194
226
|
return {
|
|
195
227
|
phase,
|
|
196
228
|
error,
|
|
@@ -217,5 +249,44 @@ export function useBojtos({ bpmn, wasm, maxEvents, }) {
|
|
|
217
249
|
runWorkers,
|
|
218
250
|
stepWorkers,
|
|
219
251
|
reset,
|
|
252
|
+
searchUserTasks,
|
|
253
|
+
searchProcessInstances,
|
|
254
|
+
searchVariables,
|
|
255
|
+
getFormByKey,
|
|
256
|
+
getResourceByKey,
|
|
257
|
+
readModelVersion,
|
|
220
258
|
};
|
|
221
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Reactively project a value out of a `readmodel` hook's read channel, re-run
|
|
262
|
+
* whenever the read model may have moved.
|
|
263
|
+
*
|
|
264
|
+
* The read queries are pull projections (see {@link ReadModelBojtosControls}),
|
|
265
|
+
* so this is the ready-made "selector" that lands their result in React state on
|
|
266
|
+
* your terms: pass the `readmodel` {@link useBojtos} controls and a `select`
|
|
267
|
+
* that calls whichever read methods you care about (with whatever filters), and
|
|
268
|
+
* the memoized result re-computes each time `readModelVersion` bumps — i.e.
|
|
269
|
+
* after every command / worker round / deploy / reset — or the load `phase`
|
|
270
|
+
* flips. Before the engine is ready the read methods return `null`, so a
|
|
271
|
+
* selector must tolerate nulls.
|
|
272
|
+
*
|
|
273
|
+
* ```tsx
|
|
274
|
+
* const run = useBojtos({ bpmn, variant: "readmodel" });
|
|
275
|
+
* const openTasks = useReadModel(
|
|
276
|
+
* run,
|
|
277
|
+
* (rm) => rm.searchUserTasks('{"state":"CREATED"}')?.items ?? [],
|
|
278
|
+
* );
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
export function useReadModel(controls, select) {
|
|
282
|
+
// Keep the latest selector and controls without making them memo dependencies:
|
|
283
|
+
// re-running is driven by the read model moving (`readModelVersion`) / readiness
|
|
284
|
+
// (`phase`), not by a fresh inline selector or a fresh `controls` object literal
|
|
285
|
+
// (`useBojtos` returns a new object each render, so depending on it directly would
|
|
286
|
+
// re-run the selector on *every* parent re-render).
|
|
287
|
+
const selectRef = useRef(select);
|
|
288
|
+
selectRef.current = select;
|
|
289
|
+
const controlsRef = useRef(controls);
|
|
290
|
+
controlsRef.current = controls;
|
|
291
|
+
return useMemo(() => selectRef.current(controlsRef.current), [controls.readModelVersion, controls.phase]);
|
|
292
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/bojtos-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"test:ci": "node --experimental-strip-types --test test/*.test.ts"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@nanobpm/bojtos-kit": "^0.
|
|
35
|
+
"@nanobpm/bojtos-kit": "^0.8.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"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,26 @@ import {
|
|
|
7
7
|
type DispatchOptions,
|
|
8
8
|
dispatchRound,
|
|
9
9
|
dispatchWorkers,
|
|
10
|
+
type EngineVariant,
|
|
11
|
+
type FormResult,
|
|
10
12
|
type JobHandler,
|
|
13
|
+
type ProcessInstanceSearchQueryResult,
|
|
14
|
+
type ReadModelBojtosSession,
|
|
15
|
+
type ResourceResult,
|
|
11
16
|
type RoundResult,
|
|
12
17
|
type Snapshot,
|
|
18
|
+
type UserTaskSearchQueryResult,
|
|
19
|
+
type VariableSearchQueryResult,
|
|
13
20
|
type WasmEvent,
|
|
14
21
|
type WasmSource,
|
|
15
22
|
} from "@nanobpm/bojtos-kit";
|
|
16
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
bpmnKey,
|
|
25
|
+
capEvents,
|
|
26
|
+
resolveVariant,
|
|
27
|
+
resourceList,
|
|
28
|
+
selectReadModel,
|
|
29
|
+
} from "./runState.js";
|
|
17
30
|
|
|
18
31
|
/** Lifecycle of the in-browser engine load. */
|
|
19
32
|
export type BojtosPhase = "loading" | "ready" | "error";
|
|
@@ -46,6 +59,22 @@ export interface UseBojtosOptions {
|
|
|
46
59
|
* whole log, which stays the default so existing consumers are unaffected.
|
|
47
60
|
*/
|
|
48
61
|
maxEvents?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Which engine variant to load, defaulting to `"lean"` — existing consumers
|
|
64
|
+
* are unaffected. Pass `"readmodel"` to also thread the gateway's
|
|
65
|
+
* Camunda-parity REST read channel through the hook: the returned controls
|
|
66
|
+
* then widen to {@link ReadModelBojtosControls}, exposing `searchUserTasks` /
|
|
67
|
+
* `searchProcessInstances` / `searchVariables` / `getFormByKey` /
|
|
68
|
+
* `getResourceByKey` plus the `readModelVersion` reactivity signal.
|
|
69
|
+
*
|
|
70
|
+
* Init-time only, like `wasm`: the variant is read when the session is first
|
|
71
|
+
* created for a given diagram, so changing it later has no effect until the
|
|
72
|
+
* next `bpmn` change re-creates the engine.
|
|
73
|
+
*
|
|
74
|
+
* The read-model binary is heavier and only code-splits in when this is
|
|
75
|
+
* `"readmodel"` (ADR 0043 §3); a lean hook never downloads it.
|
|
76
|
+
*/
|
|
77
|
+
variant?: EngineVariant;
|
|
49
78
|
}
|
|
50
79
|
|
|
51
80
|
export interface BojtosControls {
|
|
@@ -177,6 +206,72 @@ export interface BojtosControls {
|
|
|
177
206
|
reset(): void;
|
|
178
207
|
}
|
|
179
208
|
|
|
209
|
+
/**
|
|
210
|
+
* The {@link BojtosControls} of a `readmodel`-variant hook: the full command
|
|
211
|
+
* surface **plus** reactive access to the gateway's Camunda-parity REST read
|
|
212
|
+
* channel. You get one by passing `variant: "readmodel"` to {@link useBojtos},
|
|
213
|
+
* which widens the return type from `BojtosControls` to this.
|
|
214
|
+
*
|
|
215
|
+
* ## Reactivity model
|
|
216
|
+
*
|
|
217
|
+
* The read queries are **pull** projections of the read model, not part of the
|
|
218
|
+
* command→`snapshot` push loop: `searchUserTasks` et al. answer "what does the
|
|
219
|
+
* read model say *right now*", and there is no single obvious cadence at which
|
|
220
|
+
* to re-run them (a consumer may care about tasks, another about variables, each
|
|
221
|
+
* with its own filter). So rather than eagerly re-running every query after
|
|
222
|
+
* every command and stuffing five results into state, the hook exposes:
|
|
223
|
+
*
|
|
224
|
+
* - the five read methods as **imperative pulls** — call one whenever you want a
|
|
225
|
+
* fresh answer; each returns `null` before the engine is ready rather than
|
|
226
|
+
* throwing, and
|
|
227
|
+
* - {@link readModelVersion}, a counter bumped after **every** command / worker
|
|
228
|
+
* round (i.e. whenever the read model may have moved), so a consumer can make
|
|
229
|
+
* a query reactive by keying a `useMemo`/`useEffect` on it — or just let
|
|
230
|
+
* {@link useReadModel} do exactly that.
|
|
231
|
+
*
|
|
232
|
+
* This keeps the read channel opt-in and filter-agnostic while still landing its
|
|
233
|
+
* results in React state on the consumer's terms.
|
|
234
|
+
*/
|
|
235
|
+
export interface ReadModelBojtosControls extends BojtosControls {
|
|
236
|
+
/**
|
|
237
|
+
* Search user tasks through the read model (mirrors `POST
|
|
238
|
+
* /user-tasks/search`). Returns `null` until the engine is ready. Honours an
|
|
239
|
+
* optional `{ state? }` filter, e.g. `searchUserTasks('{"state":"CREATED"}')`.
|
|
240
|
+
*/
|
|
241
|
+
searchUserTasks(filterJson?: string): UserTaskSearchQueryResult | null;
|
|
242
|
+
/**
|
|
243
|
+
* Search process instances through the read model (mirrors `POST
|
|
244
|
+
* /process-instances/search`). Returns `null` until the engine is ready.
|
|
245
|
+
*/
|
|
246
|
+
searchProcessInstances(
|
|
247
|
+
filterJson?: string,
|
|
248
|
+
): ProcessInstanceSearchQueryResult | null;
|
|
249
|
+
/**
|
|
250
|
+
* Search variables through the read model (mirrors `POST
|
|
251
|
+
* /variables/search`). Returns `null` until the engine is ready.
|
|
252
|
+
*/
|
|
253
|
+
searchVariables(filterJson?: string): VariableSearchQueryResult | null;
|
|
254
|
+
/**
|
|
255
|
+
* The latest deployed form for `formKey` (mirrors `GET /forms/{formKey}`), or
|
|
256
|
+
* `null` if none exists — also `null` until the engine is ready.
|
|
257
|
+
*/
|
|
258
|
+
getFormByKey(formKey: string): FormResult | null;
|
|
259
|
+
/**
|
|
260
|
+
* The generic resource for `resourceKey` (mirrors `GET
|
|
261
|
+
* /resources/{resourceKey}`), or `null` if none exists — also `null` until the
|
|
262
|
+
* engine is ready.
|
|
263
|
+
*/
|
|
264
|
+
getResourceByKey(resourceKey: string): ResourceResult | null;
|
|
265
|
+
/**
|
|
266
|
+
* A monotonically increasing counter bumped after every command / worker round
|
|
267
|
+
* (and on deploy / reset). It is the reactivity signal for the pull read
|
|
268
|
+
* queries: key a `useMemo`/`useEffect` on it to re-run a query when the read
|
|
269
|
+
* model may have changed. {@link useReadModel} is the ready-made selector over
|
|
270
|
+
* it.
|
|
271
|
+
*/
|
|
272
|
+
readModelVersion: number;
|
|
273
|
+
}
|
|
274
|
+
|
|
180
275
|
/**
|
|
181
276
|
* Session members the hook deliberately does not re-export: the deployment
|
|
182
277
|
* lifecycle it owns itself, and the low-level activate primitive the dispatch
|
|
@@ -200,6 +295,20 @@ type UnboundCommands = Exclude<
|
|
|
200
295
|
type AssertNever<T extends never> = T;
|
|
201
296
|
type _EverySessionCommandIsBound = AssertNever<UnboundCommands>;
|
|
202
297
|
|
|
298
|
+
/**
|
|
299
|
+
* The same guard for the widened `readmodel` surface: every method of a
|
|
300
|
+
* {@link ReadModelBojtosSession} — the lean commands **and** the five read
|
|
301
|
+
* queries — must be bound on {@link ReadModelBojtosControls}, or a `readmodel`
|
|
302
|
+
* hook would silently drop part of the read channel (the exact failure mode #1
|
|
303
|
+
* described, now covering the read methods too). Adding a read query to the
|
|
304
|
+
* session without binding it here fails the build with its name.
|
|
305
|
+
*/
|
|
306
|
+
type UnboundReadModelCommands = Exclude<
|
|
307
|
+
keyof ReadModelBojtosSession,
|
|
308
|
+
NotReExported | keyof ReadModelBojtosControls
|
|
309
|
+
>;
|
|
310
|
+
type _EveryReadModelCommandIsBound = AssertNever<UnboundReadModelCommands>;
|
|
311
|
+
|
|
203
312
|
/**
|
|
204
313
|
* React binding over a headless {@link BojtosSession}: owns the engine's
|
|
205
314
|
* lifecycle and the reactive `snapshot` / `events` / `processIds` state, and
|
|
@@ -210,18 +319,51 @@ type _EverySessionCommandIsBound = AssertNever<UnboundCommands>;
|
|
|
210
319
|
* This is the reactive half of the Bojtos public API (ADR 0043 §2); the console
|
|
211
320
|
* test-run panel is its first consumer (§8 step 2 — dogfooding is the acceptance
|
|
212
321
|
* test).
|
|
322
|
+
*
|
|
323
|
+
* With `variant: "readmodel"` the return type widens to
|
|
324
|
+
* {@link ReadModelBojtosControls}, adding the read channel + `readModelVersion`;
|
|
325
|
+
* the default `"lean"` variant returns the plain {@link BojtosControls} and never
|
|
326
|
+
* downloads the heavier read-model binary.
|
|
213
327
|
*/
|
|
328
|
+
export function useBojtos(
|
|
329
|
+
options: UseBojtosOptions & { variant: "readmodel" },
|
|
330
|
+
): ReadModelBojtosControls;
|
|
331
|
+
export function useBojtos(
|
|
332
|
+
options: UseBojtosOptions & { variant?: "lean" },
|
|
333
|
+
): BojtosControls;
|
|
334
|
+
export function useBojtos(
|
|
335
|
+
options: UseBojtosOptions,
|
|
336
|
+
): BojtosControls | ReadModelBojtosControls;
|
|
214
337
|
export function useBojtos({
|
|
215
338
|
bpmn,
|
|
216
339
|
wasm,
|
|
217
340
|
maxEvents,
|
|
218
|
-
|
|
341
|
+
variant,
|
|
342
|
+
}: UseBojtosOptions): ReadModelBojtosControls {
|
|
219
343
|
const sessionRef = useRef<BojtosSession | null>(null);
|
|
344
|
+
// The same session, narrowed to its read channel, but only when we actually
|
|
345
|
+
// asked for the `readmodel` variant. Kept as its own ref (rather than casting
|
|
346
|
+
// `sessionRef`) so the read methods reach the query surface without a cast — a
|
|
347
|
+
// lean session simply leaves this null and every read pull returns null.
|
|
348
|
+
const readModelRef = useRef<ReadModelBojtosSession | null>(null);
|
|
220
349
|
const [phase, setPhase] = useState<BojtosPhase>("loading");
|
|
221
350
|
const [error, setError] = useState<string | null>(null);
|
|
222
351
|
const [processIds, setProcessIds] = useState<string[]>([]);
|
|
223
352
|
const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
|
|
224
353
|
const [events, setEvents] = useState<WasmEvent[]>([]);
|
|
354
|
+
// Bumped whenever the read model may have moved (any command / round / deploy /
|
|
355
|
+
// reset) so pull read queries can be made reactive by keying on it.
|
|
356
|
+
const [readModelVersion, setReadModelVersion] = useState(0);
|
|
357
|
+
const bumpReadModel = useCallback(
|
|
358
|
+
() => setReadModelVersion((v) => v + 1),
|
|
359
|
+
[],
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// The variant is an init-time concern like `wasm` (read when a session is
|
|
363
|
+
// first created for a diagram), so keep it in a ref rather than the deploy
|
|
364
|
+
// effect's deps.
|
|
365
|
+
const variantRef = useRef(variant);
|
|
366
|
+
variantRef.current = variant;
|
|
225
367
|
|
|
226
368
|
// The wasm source is an init-time concern (the first `ensureWasm` wins), so
|
|
227
369
|
// keep it in a ref rather than the mount effect's deps — a fresh URL/bytes
|
|
@@ -254,6 +396,9 @@ export function useBojtos({
|
|
|
254
396
|
setSnapshot(null);
|
|
255
397
|
setEvents([]);
|
|
256
398
|
setError(null);
|
|
399
|
+
// The read model was just wiped and re-seeded by the redeploy, so any
|
|
400
|
+
// reactive read query must re-run.
|
|
401
|
+
bumpReadModel();
|
|
257
402
|
},
|
|
258
403
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
259
404
|
[deployKey],
|
|
@@ -270,7 +415,15 @@ export function useBojtos({
|
|
|
270
415
|
setSnapshot(null);
|
|
271
416
|
setEvents([]);
|
|
272
417
|
setError(null);
|
|
273
|
-
|
|
418
|
+
// Resolve the session with the requested variant. The `readmodel` branch
|
|
419
|
+
// keeps the narrowed `ReadModelBojtosSession` so the read methods reach the
|
|
420
|
+
// query surface without a cast; the lean branch leaves `readModelRef` null.
|
|
421
|
+
const variant = resolveVariant(variantRef.current);
|
|
422
|
+
const pending =
|
|
423
|
+
variant === "readmodel"
|
|
424
|
+
? createBojtosSession({ wasm: wasmRef.current, variant: "readmodel" })
|
|
425
|
+
: createBojtosSession({ wasm: wasmRef.current });
|
|
426
|
+
pending
|
|
274
427
|
.then((session) => {
|
|
275
428
|
if (cancelled) {
|
|
276
429
|
session.free();
|
|
@@ -288,6 +441,8 @@ export function useBojtos({
|
|
|
288
441
|
return;
|
|
289
442
|
}
|
|
290
443
|
sessionRef.current = session;
|
|
444
|
+
readModelRef.current =
|
|
445
|
+
variant === "readmodel" ? (session as ReadModelBojtosSession) : null;
|
|
291
446
|
setPhase("ready");
|
|
292
447
|
})
|
|
293
448
|
.catch((e) => {
|
|
@@ -299,6 +454,7 @@ export function useBojtos({
|
|
|
299
454
|
cancelled = true;
|
|
300
455
|
sessionRef.current?.free();
|
|
301
456
|
sessionRef.current = null;
|
|
457
|
+
readModelRef.current = null;
|
|
302
458
|
};
|
|
303
459
|
}, [deployInto]);
|
|
304
460
|
|
|
@@ -311,13 +467,15 @@ export function useBojtos({
|
|
|
311
467
|
setSnapshot(snap);
|
|
312
468
|
setEvents(readEvents(session));
|
|
313
469
|
setError(null);
|
|
470
|
+
// A command may have moved the read model; signal reactive readers.
|
|
471
|
+
bumpReadModel();
|
|
314
472
|
return snap;
|
|
315
473
|
} catch (e) {
|
|
316
474
|
setError(String(e));
|
|
317
475
|
return null;
|
|
318
476
|
}
|
|
319
477
|
},
|
|
320
|
-
[],
|
|
478
|
+
[bumpReadModel, readEvents],
|
|
321
479
|
);
|
|
322
480
|
|
|
323
481
|
const createInstance = useCallback(
|
|
@@ -431,6 +589,7 @@ export function useBojtos({
|
|
|
431
589
|
setSnapshot(settled);
|
|
432
590
|
setEvents(readEvents(session));
|
|
433
591
|
setError(null);
|
|
592
|
+
bumpReadModel();
|
|
434
593
|
return settled;
|
|
435
594
|
} catch (e) {
|
|
436
595
|
if (sessionRef.current !== session) return null;
|
|
@@ -439,10 +598,11 @@ export function useBojtos({
|
|
|
439
598
|
setSnapshot(session.snapshot());
|
|
440
599
|
setEvents(readEvents(session));
|
|
441
600
|
setError(String(e));
|
|
601
|
+
bumpReadModel();
|
|
442
602
|
return null;
|
|
443
603
|
}
|
|
444
604
|
},
|
|
445
|
-
[],
|
|
605
|
+
[bumpReadModel, readEvents],
|
|
446
606
|
);
|
|
447
607
|
|
|
448
608
|
const stepWorkers = useCallback(
|
|
@@ -459,16 +619,18 @@ export function useBojtos({
|
|
|
459
619
|
setSnapshot(round.snapshot);
|
|
460
620
|
setEvents(readEvents(session));
|
|
461
621
|
setError(null);
|
|
622
|
+
bumpReadModel();
|
|
462
623
|
return round;
|
|
463
624
|
} catch (e) {
|
|
464
625
|
if (sessionRef.current !== session) return null;
|
|
465
626
|
setSnapshot(session.snapshot());
|
|
466
627
|
setEvents(readEvents(session));
|
|
467
628
|
setError(String(e));
|
|
629
|
+
bumpReadModel();
|
|
468
630
|
return null;
|
|
469
631
|
}
|
|
470
632
|
},
|
|
471
|
-
[],
|
|
633
|
+
[bumpReadModel, readEvents],
|
|
472
634
|
);
|
|
473
635
|
|
|
474
636
|
const reset = useCallback(() => {
|
|
@@ -485,6 +647,45 @@ export function useBojtos({
|
|
|
485
647
|
}
|
|
486
648
|
}, [deployInto]);
|
|
487
649
|
|
|
650
|
+
// The read channel. Each pull returns null when there is no live read-model
|
|
651
|
+
// session (loading, or a lean-variant hook), via the shared `selectReadModel`
|
|
652
|
+
// guard, rather than throwing on a missing engine. They intentionally do not
|
|
653
|
+
// touch React state themselves — reactivity is opt-in through
|
|
654
|
+
// `readModelVersion` / `useReadModel` (see `ReadModelBojtosControls`).
|
|
655
|
+
const searchUserTasks = useCallback(
|
|
656
|
+
(filterJson?: string) =>
|
|
657
|
+
selectReadModel(readModelRef.current, (rm) =>
|
|
658
|
+
rm.searchUserTasks(filterJson),
|
|
659
|
+
),
|
|
660
|
+
[],
|
|
661
|
+
);
|
|
662
|
+
const searchProcessInstances = useCallback(
|
|
663
|
+
(filterJson?: string) =>
|
|
664
|
+
selectReadModel(readModelRef.current, (rm) =>
|
|
665
|
+
rm.searchProcessInstances(filterJson),
|
|
666
|
+
),
|
|
667
|
+
[],
|
|
668
|
+
);
|
|
669
|
+
const searchVariables = useCallback(
|
|
670
|
+
(filterJson?: string) =>
|
|
671
|
+
selectReadModel(readModelRef.current, (rm) =>
|
|
672
|
+
rm.searchVariables(filterJson),
|
|
673
|
+
),
|
|
674
|
+
[],
|
|
675
|
+
);
|
|
676
|
+
const getFormByKey = useCallback(
|
|
677
|
+
(formKey: string) =>
|
|
678
|
+
selectReadModel(readModelRef.current, (rm) => rm.getFormByKey(formKey)),
|
|
679
|
+
[],
|
|
680
|
+
);
|
|
681
|
+
const getResourceByKey = useCallback(
|
|
682
|
+
(resourceKey: string) =>
|
|
683
|
+
selectReadModel(readModelRef.current, (rm) =>
|
|
684
|
+
rm.getResourceByKey(resourceKey),
|
|
685
|
+
),
|
|
686
|
+
[],
|
|
687
|
+
);
|
|
688
|
+
|
|
488
689
|
return {
|
|
489
690
|
phase,
|
|
490
691
|
error,
|
|
@@ -511,5 +712,51 @@ export function useBojtos({
|
|
|
511
712
|
runWorkers,
|
|
512
713
|
stepWorkers,
|
|
513
714
|
reset,
|
|
715
|
+
searchUserTasks,
|
|
716
|
+
searchProcessInstances,
|
|
717
|
+
searchVariables,
|
|
718
|
+
getFormByKey,
|
|
719
|
+
getResourceByKey,
|
|
720
|
+
readModelVersion,
|
|
514
721
|
};
|
|
515
722
|
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Reactively project a value out of a `readmodel` hook's read channel, re-run
|
|
726
|
+
* whenever the read model may have moved.
|
|
727
|
+
*
|
|
728
|
+
* The read queries are pull projections (see {@link ReadModelBojtosControls}),
|
|
729
|
+
* so this is the ready-made "selector" that lands their result in React state on
|
|
730
|
+
* your terms: pass the `readmodel` {@link useBojtos} controls and a `select`
|
|
731
|
+
* that calls whichever read methods you care about (with whatever filters), and
|
|
732
|
+
* the memoized result re-computes each time `readModelVersion` bumps — i.e.
|
|
733
|
+
* after every command / worker round / deploy / reset — or the load `phase`
|
|
734
|
+
* flips. Before the engine is ready the read methods return `null`, so a
|
|
735
|
+
* selector must tolerate nulls.
|
|
736
|
+
*
|
|
737
|
+
* ```tsx
|
|
738
|
+
* const run = useBojtos({ bpmn, variant: "readmodel" });
|
|
739
|
+
* const openTasks = useReadModel(
|
|
740
|
+
* run,
|
|
741
|
+
* (rm) => rm.searchUserTasks('{"state":"CREATED"}')?.items ?? [],
|
|
742
|
+
* );
|
|
743
|
+
* ```
|
|
744
|
+
*/
|
|
745
|
+
export function useReadModel<T>(
|
|
746
|
+
controls: ReadModelBojtosControls,
|
|
747
|
+
select: (controls: ReadModelBojtosControls) => T,
|
|
748
|
+
): T {
|
|
749
|
+
// Keep the latest selector and controls without making them memo dependencies:
|
|
750
|
+
// re-running is driven by the read model moving (`readModelVersion`) / readiness
|
|
751
|
+
// (`phase`), not by a fresh inline selector or a fresh `controls` object literal
|
|
752
|
+
// (`useBojtos` returns a new object each render, so depending on it directly would
|
|
753
|
+
// re-run the selector on *every* parent re-render).
|
|
754
|
+
const selectRef = useRef(select);
|
|
755
|
+
selectRef.current = select;
|
|
756
|
+
const controlsRef = useRef(controls);
|
|
757
|
+
controlsRef.current = controls;
|
|
758
|
+
return useMemo(
|
|
759
|
+
() => selectRef.current(controlsRef.current),
|
|
760
|
+
[controls.readModelVersion, controls.phase],
|
|
761
|
+
);
|
|
762
|
+
}
|