@nanobpm/bojtos-react 0.4.0 → 0.6.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/src/useBojtos.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import {
3
+ type ActivateInstruction,
3
4
  type AgentResult,
4
5
  type BojtosSession,
5
6
  createBojtosSession,
@@ -12,13 +13,20 @@ import {
12
13
  type WasmEvent,
13
14
  type WasmSource,
14
15
  } from "@nanobpm/bojtos-kit";
16
+ import { bpmnKey, capEvents, resourceList } from "./runState.js";
15
17
 
16
18
  /** Lifecycle of the in-browser engine load. */
17
19
  export type BojtosPhase = "loading" | "ready" | "error";
18
20
 
19
21
  export interface UseBojtosOptions {
20
- /** The BPMN diagram XML to deploy. Re-deploys on a fresh engine when it changes. */
21
- bpmn: string;
22
+ /**
23
+ * The BPMN to deploy. Re-deploys on a fresh engine when it changes.
24
+ *
25
+ * Pass an array to deploy several resources into one engine — a called
26
+ * process alongside its parent, say. `processIds` then lists every deployable
27
+ * process across all of them, in deployment order.
28
+ */
29
+ bpmn: string | string[];
22
30
  /**
23
31
  * Optional engine wasm source. Pass a `URL` / bytes / `WebAssembly.Module`
24
32
  * when the default `import.meta.url` loader can't resolve the binary (the
@@ -29,6 +37,15 @@ export interface UseBojtosOptions {
29
37
  * reload the module.
30
38
  */
31
39
  wasm?: WasmSource;
40
+ /**
41
+ * Cap the reactive `events` log at the most recent N entries.
42
+ *
43
+ * Every command re-reads the engine's full event log into React state, so a
44
+ * long-running demo copies an ever-growing array on each step. Set this when
45
+ * a page runs for a while and only shows a tail; leave it unset to keep the
46
+ * whole log, which stays the default so existing consumers are unaffected.
47
+ */
48
+ maxEvents?: number;
32
49
  }
33
50
 
34
51
  export interface BojtosControls {
@@ -66,6 +83,74 @@ export interface BojtosControls {
66
83
  ): Snapshot | null;
67
84
  /** Advance the virtual clock. */
68
85
  advanceTime(byMs: number): Snapshot | null;
86
+ /**
87
+ * Throw a BPMN business error from a waiting job: interrupts the activity via
88
+ * a matching error boundary/event-subprocess catch, or raises an incident if
89
+ * uncaught. The job is consumed either way.
90
+ */
91
+ throwError(
92
+ jobKey: string,
93
+ errorCode: string,
94
+ errorMessage: string,
95
+ ): Snapshot | null;
96
+ /**
97
+ * Set a job's remaining retries. Used to recover a job parked on a no-retries
98
+ * incident before resolving that incident; does not itself unblock the job.
99
+ */
100
+ updateRetries(jobKey: string, retries: number): Snapshot | null;
101
+ /**
102
+ * Resolve an open incident by key, retrying the work that failed. Pair with
103
+ * {@link updateRetries} to make a failed job activatable again — the
104
+ * incident/retry loop a demo needs to show recovery.
105
+ */
106
+ resolveIncident(incidentKey: string): Snapshot | null;
107
+ /**
108
+ * Merge variables into a scope (a process-instance or element-instance key).
109
+ * With `local`, they are written strictly into that scope; otherwise they
110
+ * propagate up to the nearest ancestor defining each name.
111
+ */
112
+ setVariables(
113
+ scopeKey: string,
114
+ variablesJson: string,
115
+ local: boolean,
116
+ ): Snapshot | null;
117
+ /** Broadcast a signal by name to every matching open subscription. */
118
+ broadcastSignal(signalName: string, variablesJson: string): Snapshot | null;
119
+ /** Cancel (terminate) a running process instance. */
120
+ cancelInstance(instanceKey: string): Snapshot | null;
121
+ /**
122
+ * Modify a running instance: terminate element instances and/or activate new
123
+ * ones (Zeebe "modify process instance").
124
+ */
125
+ modify(
126
+ instanceKey: string,
127
+ activateInstructions: ActivateInstruction[],
128
+ terminateElementInstanceKeys: string[],
129
+ ): Snapshot | null;
130
+ /**
131
+ * Complete a waiting user task, merging output variables.
132
+ *
133
+ * A `userTask` produces no job, so the dispatch loop cannot advance one: this
134
+ * is the only way a model with a human step reaches its end event. Drive it
135
+ * from `snapshot.userTasks`.
136
+ */
137
+ completeUserTask(userTaskKey: string, variablesJson: string): Snapshot | null;
138
+ /**
139
+ * Assign a user task. With `allowOverride` false the command is rejected if
140
+ * the task already has an assignee.
141
+ */
142
+ assignUserTask(
143
+ userTaskKey: string,
144
+ assignee: string,
145
+ allowOverride: boolean,
146
+ ): Snapshot | null;
147
+ /** Clear a user task's assignee. */
148
+ unassignUserTask(userTaskKey: string): Snapshot | null;
149
+ /**
150
+ * Update a user task's attributes from a JSON changeset (`candidateGroups`,
151
+ * `candidateUsers`, `dueDate`, `followUpDate`, `priority`).
152
+ */
153
+ updateUserTask(userTaskKey: string, changesetJson: string): Snapshot | null;
69
154
  /**
70
155
  * Run the registered worker handlers until the process settles (activate →
71
156
  * handler → complete/fail), then reflect the resulting snapshot/events.
@@ -92,6 +177,29 @@ export interface BojtosControls {
92
177
  reset(): void;
93
178
  }
94
179
 
180
+ /**
181
+ * Session members the hook deliberately does not re-export: the deployment
182
+ * lifecycle it owns itself, and the low-level activate primitive the dispatch
183
+ * loop owns.
184
+ */
185
+ type NotReExported = "deploy" | "free" | "activateJobs";
186
+
187
+ /**
188
+ * Compile-time guard. `useBojtos` keeps its session private, so a command it
189
+ * doesn't re-export is *unreachable* for a consumer rather than merely
190
+ * inconvenient — which is how `completeUserTask` went missing and left any model
191
+ * with a user task unfinishable (#1).
192
+ *
193
+ * Adding a command to {@link BojtosSession} without a binding here now fails the
194
+ * build with the offending name, instead of shipping a hole.
195
+ */
196
+ type UnboundCommands = Exclude<
197
+ keyof BojtosSession,
198
+ NotReExported | keyof BojtosControls
199
+ >;
200
+ type AssertNever<T extends never> = T;
201
+ type _EverySessionCommandIsBound = AssertNever<UnboundCommands>;
202
+
95
203
  /**
96
204
  * React binding over a headless {@link BojtosSession}: owns the engine's
97
205
  * lifecycle and the reactive `snapshot` / `events` / `processIds` state, and
@@ -103,7 +211,11 @@ export interface BojtosControls {
103
211
  * test-run panel is its first consumer (§8 step 2 — dogfooding is the acceptance
104
212
  * test).
105
213
  */
106
- export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
214
+ export function useBojtos({
215
+ bpmn,
216
+ wasm,
217
+ maxEvents,
218
+ }: UseBojtosOptions): BojtosControls {
107
219
  const sessionRef = useRef<BojtosSession | null>(null);
108
220
  const [phase, setPhase] = useState<BojtosPhase>("loading");
109
221
  const [error, setError] = useState<string | null>(null);
@@ -117,15 +229,34 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
117
229
  const wasmRef = useRef(wasm);
118
230
  wasmRef.current = wasm;
119
231
 
232
+ // An array prop has a fresh identity every render, which would re-create the
233
+ // engine on each one. Key the deploy effect on the content instead — see
234
+ // `bpmnKey` for why this is a boundary-preserving serialization, not a join.
235
+ const deployKey = bpmnKey(bpmn);
236
+ const bpmnRef = useRef(bpmn);
237
+ bpmnRef.current = bpmn;
238
+
239
+ // Trim the reactive event log when the consumer asked for a cap.
240
+ const maxEventsRef = useRef(maxEvents);
241
+ maxEventsRef.current = maxEvents;
242
+ const readEvents = useCallback((session: BojtosSession): WasmEvent[] => {
243
+ return capEvents(session.events(), maxEventsRef.current);
244
+ }, []);
245
+
120
246
  const deployInto = useCallback(
121
247
  (session: BojtosSession) => {
122
- const res = session.deploy(bpmn);
123
- setProcessIds(res.processIds);
248
+ const resources = resourceList(bpmnRef.current);
249
+ // Deploy in order, collecting every deployable process id. A later
250
+ // resource can reference an earlier one (a call activity's child).
251
+ const ids: string[] = [];
252
+ for (const xml of resources) ids.push(...session.deploy(xml).processIds);
253
+ setProcessIds(ids);
124
254
  setSnapshot(null);
125
255
  setEvents([]);
126
256
  setError(null);
127
257
  },
128
- [bpmn],
258
+ // eslint-disable-next-line react-hooks/exhaustive-deps
259
+ [deployKey],
129
260
  );
130
261
 
131
262
  useEffect(() => {
@@ -178,7 +309,7 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
178
309
  try {
179
310
  const snap = fn(session);
180
311
  setSnapshot(snap);
181
- setEvents(session.events());
312
+ setEvents(readEvents(session));
182
313
  setError(null);
183
314
  return snap;
184
315
  } catch (e) {
@@ -218,6 +349,68 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
218
349
  run((s) => s.correlateMessage(messageName, correlationKey, variablesJson)),
219
350
  [run],
220
351
  );
352
+ const throwError = useCallback(
353
+ (jobKey: string, errorCode: string, errorMessage: string) =>
354
+ run((s) => s.throwError(jobKey, errorCode, errorMessage)),
355
+ [run],
356
+ );
357
+ const updateRetries = useCallback(
358
+ (jobKey: string, retries: number) =>
359
+ run((s) => s.updateRetries(jobKey, retries)),
360
+ [run],
361
+ );
362
+ const resolveIncident = useCallback(
363
+ (incidentKey: string) => run((s) => s.resolveIncident(incidentKey)),
364
+ [run],
365
+ );
366
+ const setVariables = useCallback(
367
+ (scopeKey: string, variablesJson: string, local: boolean) =>
368
+ run((s) => s.setVariables(scopeKey, variablesJson, local)),
369
+ [run],
370
+ );
371
+ const broadcastSignal = useCallback(
372
+ (signalName: string, variablesJson: string) =>
373
+ run((s) => s.broadcastSignal(signalName, variablesJson)),
374
+ [run],
375
+ );
376
+ const cancelInstance = useCallback(
377
+ (instanceKey: string) => run((s) => s.cancelInstance(instanceKey)),
378
+ [run],
379
+ );
380
+ const modify = useCallback(
381
+ (
382
+ instanceKey: string,
383
+ activateInstructions: ActivateInstruction[],
384
+ terminateElementInstanceKeys: string[],
385
+ ) =>
386
+ run((s) =>
387
+ s.modify(
388
+ instanceKey,
389
+ activateInstructions,
390
+ terminateElementInstanceKeys,
391
+ ),
392
+ ),
393
+ [run],
394
+ );
395
+ const completeUserTask = useCallback(
396
+ (userTaskKey: string, variablesJson: string) =>
397
+ run((s) => s.completeUserTask(userTaskKey, variablesJson)),
398
+ [run],
399
+ );
400
+ const assignUserTask = useCallback(
401
+ (userTaskKey: string, assignee: string, allowOverride: boolean) =>
402
+ run((s) => s.assignUserTask(userTaskKey, assignee, allowOverride)),
403
+ [run],
404
+ );
405
+ const unassignUserTask = useCallback(
406
+ (userTaskKey: string) => run((s) => s.unassignUserTask(userTaskKey)),
407
+ [run],
408
+ );
409
+ const updateUserTask = useCallback(
410
+ (userTaskKey: string, changesetJson: string) =>
411
+ run((s) => s.updateUserTask(userTaskKey, changesetJson)),
412
+ [run],
413
+ );
221
414
 
222
415
  const runWorkers = useCallback(
223
416
  async (
@@ -236,7 +429,7 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
236
429
  // while we awaited — don't publish stale state or read a freed session.
237
430
  if (sessionRef.current !== session) return null;
238
431
  setSnapshot(settled);
239
- setEvents(session.events());
432
+ setEvents(readEvents(session));
240
433
  setError(null);
241
434
  return settled;
242
435
  } catch (e) {
@@ -244,7 +437,7 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
244
437
  // Reflect whatever state the engine reached before the drain aborted
245
438
  // (e.g. the maxRounds guard) so the view isn't left stale.
246
439
  setSnapshot(session.snapshot());
247
- setEvents(session.events());
440
+ setEvents(readEvents(session));
248
441
  setError(String(e));
249
442
  return null;
250
443
  }
@@ -264,13 +457,13 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
264
457
  // Bail if the session was replaced/freed while we awaited the round.
265
458
  if (sessionRef.current !== session) return null;
266
459
  setSnapshot(round.snapshot);
267
- setEvents(session.events());
460
+ setEvents(readEvents(session));
268
461
  setError(null);
269
462
  return round;
270
463
  } catch (e) {
271
464
  if (sessionRef.current !== session) return null;
272
465
  setSnapshot(session.snapshot());
273
- setEvents(session.events());
466
+ setEvents(readEvents(session));
274
467
  setError(String(e));
275
468
  return null;
276
469
  }
@@ -304,6 +497,17 @@ export function useBojtos({ bpmn, wasm }: UseBojtosOptions): BojtosControls {
304
497
  failJob,
305
498
  advanceTime,
306
499
  correlateMessage,
500
+ throwError,
501
+ updateRetries,
502
+ resolveIncident,
503
+ setVariables,
504
+ broadcastSignal,
505
+ cancelInstance,
506
+ modify,
507
+ completeUserTask,
508
+ assignUserTask,
509
+ unassignUserTask,
510
+ updateUserTask,
307
511
  runWorkers,
308
512
  stepWorkers,
309
513
  reset,