@nanobpm/bojtos-kit 0.8.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 CHANGED
@@ -61,6 +61,28 @@ The read methods — `searchUserTasks`, `searchProcessInstances`,
61
61
  from `@nanobpm/engine-wasm/readmodel-types`, which are **derived** from the
62
62
  Camunda-parity REST OpenAPI (one source of truth, not a hand-copy).
63
63
 
64
+ ### `readModel()` — the `@nanobpm/engine-testkit` assertion handle
65
+
66
+ A `readmodel`-variant session also exposes `readModel()`, which returns the
67
+ session's engine read model as `@nanobpm/engine-testkit`'s structural
68
+ [`EngineReadModel`](https://www.npmjs.com/package/@nanobpm/engine-testkit) port
69
+ (`snapshot()` + the user-task read channel). Hand it straight to the `assertThat*`
70
+ DSL — it reads the engine's canonical snapshot off *this* session, so there is no
71
+ re-derived copy of the state:
72
+
73
+ ```ts
74
+ import { assertThatInstance, assertThatUserTask, byProcessId } from "@nanobpm/engine-testkit";
75
+
76
+ const rm = session.readModel(); // typed EngineReadModel — a compile-time guarantee
77
+
78
+ assertThatInstance(rm, byProcessId("review")).isActive().hasActiveElement("review-task");
79
+ await assertThatUserTask(rm, { elementId: "review-task" }).isCreated();
80
+ ```
81
+
82
+ The React binding surfaces the same handle as `useBojtos({ variant: "readmodel" }).readModel()`
83
+ (`EngineReadModel | null` until the engine is ready).
84
+
85
+
64
86
  ## Trace model
65
87
 
66
88
  The kit also holds the framework-agnostic **trace model** the shared
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { ensureWasm, ensureReadModelWasm, createBojtosSession, type BojtosSession, type ReadModelBojtosSession, type EngineVariant, type WasmSource, } from "./session.js";
2
+ export type { EngineReadModel, UserTaskQuery, UserTaskRow, } from "@nanobpm/engine-testkit";
2
3
  export { dispatchWorkers, dispatchRound, settleReason, unhandledJobTypes, JobFailure, type JobHandler, type JobResult, type AgentHandler, type DispatchOptions, type DispatchResult, type RoundResult, type SettleReason, } from "./worker.js";
3
4
  export { buildTraceItems, isTraceTurnGroup, foldEngineEvents, traceEntriesToRows, } from "./trace.js";
4
5
  export type { TraceRowKind, TraceEntry, TraceRow, TraceTurnGroup, TraceItem, TraceAdapter, } from "./trace.js";
package/dist/session.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type InitInput } from "@nanobpm/engine-wasm";
2
2
  import type { FormResult, ProcessInstanceSearchQueryResult, ResourceResult, UserTaskSearchQueryResult, VariableSearchQueryResult } from "@nanobpm/engine-wasm/readmodel-types";
3
+ import type { EngineReadModel } from "@nanobpm/engine-testkit";
3
4
  import type { ActivatedJob, ActivateInstruction, AgentResult, Snapshot, WasmEvent } from "./types.js";
4
5
  /**
5
6
  * Which engine binary backs a session. The two are separate wasm builds
@@ -204,6 +205,18 @@ export interface ReadModelBojtosSession extends BojtosSession {
204
205
  * `GET /resources/{resourceKey}`.
205
206
  */
206
207
  getResourceByKey(resourceKey: string): ResourceResult | null;
208
+ /**
209
+ * This session's engine read model as the structural {@link EngineReadModel}
210
+ * port that `@nanobpm/engine-testkit`'s `assertThat*` DSL asserts over. Hand it
211
+ * straight to `assertThatInstance` / `assertThatUserTask`: it reads the
212
+ * canonical engine snapshot (`snapshot()`) and the user-task read channel
213
+ * (`searchUserTasks` / `openUserTasks`, the CREATED set) off *this* session, so
214
+ * a consumer (e.g. `camunda/web-demo-framework`) asserts against the engine's
215
+ * single source of truth rather than re-deriving a snapshot. The returned
216
+ * object is typed `EngineReadModel`, so its structural conformance to the port
217
+ * is a compile-time guarantee, not a runtime cast.
218
+ */
219
+ readModel(): EngineReadModel;
207
220
  }
208
221
  /**
209
222
  * Create a fresh headless engine session. Ensures the chosen wasm variant is
package/dist/session.js CHANGED
@@ -154,6 +154,46 @@ class WasmReadModelSession extends WasmBojtosSession {
154
154
  getResourceByKey(resourceKey) {
155
155
  return JSON.parse(this.rm.getResourceByKey(resourceKey));
156
156
  }
157
+ readModel() {
158
+ // Reads flow straight off this session's engine — the same parsed snapshot
159
+ // and the same user-task read channel the rest of the session exposes — so
160
+ // there is a single source of truth and no re-derived copy of the state
161
+ // (nanobpm/bojtos#15). Typing the returned object as `EngineReadModel` makes
162
+ // structural conformance to engine-testkit's port a compile-time assignment.
163
+ const searchRows = (query) => this.searchUserTasks(JSON.stringify(query.state === undefined ? {} : { state: query.state }))
164
+ .items.filter((task) => {
165
+ // The wasm read model honours the lifecycle `state` filter itself; the
166
+ // non-lifecycle narrowings the DSL relies on (processInstanceKey /
167
+ // assignee / candidateGroup) are applied here so `assertThatUserTask`'s
168
+ // `hasAssignee` / `hasCandidateGroup` / instance-scoped selects hold.
169
+ if (query.processInstanceKey !== undefined &&
170
+ task.processInstanceKey !== query.processInstanceKey) {
171
+ return false;
172
+ }
173
+ if (query.assignee !== undefined && task.assignee !== query.assignee) {
174
+ return false;
175
+ }
176
+ if (query.candidateGroup !== undefined &&
177
+ !task.candidateGroups.includes(query.candidateGroup)) {
178
+ return false;
179
+ }
180
+ return true;
181
+ })
182
+ .map((task) => ({
183
+ userTaskKey: task.userTaskKey,
184
+ elementId: task.elementId ?? undefined,
185
+ }));
186
+ return {
187
+ // The engine snapshot is already the DSL's expected structural shape
188
+ // (`instances`/`activeElements`/`elementStats`/`incidents`/per-instance
189
+ // `variables`); the assertion only retypes the same object as the port's
190
+ // open `Record<string, unknown>` — it does not re-derive it.
191
+ snapshot: () => this.snapshot(),
192
+ searchUserTasks: (query) => Promise.resolve(searchRows(query)),
193
+ // openUserTasks is searchUserTasks pinned to the open (CREATED) set.
194
+ openUserTasks: (query) => Promise.resolve(searchRows({ ...query, state: "CREATED" })),
195
+ };
196
+ }
157
197
  }
158
198
  export async function createBojtosSession(opts) {
159
199
  if (opts?.variant === "readmodel") {
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@nanobpm/bojtos-kit",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Framework-agnostic core of the Bojtos in-browser BPMN demo framework (ADR 0043): a single scenario runner over the @nanobpm/engine-wasm engine (deploy, start instances, complete/fail jobs, advance the clock, read snapshots and the event log), plus the engine's snapshot/event contract types. Consumed by @nanobpm/bojtos-react and the console test-run panel.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
+ "engines": {
8
+ "node": ">=22.6"
9
+ },
7
10
  "repository": {
8
11
  "type": "git",
9
12
  "url": "git+https://github.com/nanobpm/bojtos.git",
@@ -32,6 +35,7 @@
32
35
  "prepack": "npm run build"
33
36
  },
34
37
  "dependencies": {
38
+ "@nanobpm/engine-testkit": "^0.1.0",
35
39
  "@nanobpm/engine-wasm": "^0.7.0"
36
40
  },
37
41
  "devDependencies": {
package/src/index.ts CHANGED
@@ -11,6 +11,14 @@ export {
11
11
  type EngineVariant,
12
12
  type WasmSource,
13
13
  } from "./session.js";
14
+ // The engine-testkit read-model port a `ReadModelBojtosSession.readModel()`
15
+ // satisfies, re-exported so a consumer can name the handle's type (e.g. to store
16
+ // it) without a direct import of `@nanobpm/engine-testkit`.
17
+ export type {
18
+ EngineReadModel,
19
+ UserTaskQuery,
20
+ UserTaskRow,
21
+ } from "@nanobpm/engine-testkit";
14
22
  export {
15
23
  dispatchWorkers,
16
24
  dispatchRound,
package/src/session.ts CHANGED
@@ -6,6 +6,11 @@ import type {
6
6
  UserTaskSearchQueryResult,
7
7
  VariableSearchQueryResult,
8
8
  } from "@nanobpm/engine-wasm/readmodel-types";
9
+ import type {
10
+ EngineReadModel,
11
+ UserTaskQuery,
12
+ UserTaskRow,
13
+ } from "@nanobpm/engine-testkit";
9
14
  import type {
10
15
  ActivatedJob,
11
16
  ActivateInstruction,
@@ -282,6 +287,18 @@ export interface ReadModelBojtosSession extends BojtosSession {
282
287
  * `GET /resources/{resourceKey}`.
283
288
  */
284
289
  getResourceByKey(resourceKey: string): ResourceResult | null;
290
+ /**
291
+ * This session's engine read model as the structural {@link EngineReadModel}
292
+ * port that `@nanobpm/engine-testkit`'s `assertThat*` DSL asserts over. Hand it
293
+ * straight to `assertThatInstance` / `assertThatUserTask`: it reads the
294
+ * canonical engine snapshot (`snapshot()`) and the user-task read channel
295
+ * (`searchUserTasks` / `openUserTasks`, the CREATED set) off *this* session, so
296
+ * a consumer (e.g. `camunda/web-demo-framework`) asserts against the engine's
297
+ * single source of truth rather than re-deriving a snapshot. The returned
298
+ * object is typed `EngineReadModel`, so its structural conformance to the port
299
+ * is a compile-time guarantee, not a runtime cast.
300
+ */
301
+ readModel(): EngineReadModel;
285
302
  }
286
303
 
287
304
  function parseSnapshot(json: string): Snapshot {
@@ -492,6 +509,56 @@ class WasmReadModelSession
492
509
  this.rm.getResourceByKey(resourceKey),
493
510
  ) as ResourceResult | null;
494
511
  }
512
+
513
+ readModel(): EngineReadModel {
514
+ // Reads flow straight off this session's engine — the same parsed snapshot
515
+ // and the same user-task read channel the rest of the session exposes — so
516
+ // there is a single source of truth and no re-derived copy of the state
517
+ // (nanobpm/bojtos#15). Typing the returned object as `EngineReadModel` makes
518
+ // structural conformance to engine-testkit's port a compile-time assignment.
519
+ const searchRows = (query: UserTaskQuery): UserTaskRow[] =>
520
+ this.searchUserTasks(
521
+ JSON.stringify(query.state === undefined ? {} : { state: query.state }),
522
+ )
523
+ .items.filter((task) => {
524
+ // The wasm read model honours the lifecycle `state` filter itself; the
525
+ // non-lifecycle narrowings the DSL relies on (processInstanceKey /
526
+ // assignee / candidateGroup) are applied here so `assertThatUserTask`'s
527
+ // `hasAssignee` / `hasCandidateGroup` / instance-scoped selects hold.
528
+ if (
529
+ query.processInstanceKey !== undefined &&
530
+ task.processInstanceKey !== query.processInstanceKey
531
+ ) {
532
+ return false;
533
+ }
534
+ if (query.assignee !== undefined && task.assignee !== query.assignee) {
535
+ return false;
536
+ }
537
+ if (
538
+ query.candidateGroup !== undefined &&
539
+ !task.candidateGroups.includes(query.candidateGroup)
540
+ ) {
541
+ return false;
542
+ }
543
+ return true;
544
+ })
545
+ .map((task) => ({
546
+ userTaskKey: task.userTaskKey,
547
+ elementId: task.elementId ?? undefined,
548
+ }));
549
+
550
+ return {
551
+ // The engine snapshot is already the DSL's expected structural shape
552
+ // (`instances`/`activeElements`/`elementStats`/`incidents`/per-instance
553
+ // `variables`); the assertion only retypes the same object as the port's
554
+ // open `Record<string, unknown>` — it does not re-derive it.
555
+ snapshot: () => this.snapshot() as unknown as Record<string, unknown>,
556
+ searchUserTasks: (query) => Promise.resolve(searchRows(query)),
557
+ // openUserTasks is searchUserTasks pinned to the open (CREATED) set.
558
+ openUserTasks: (query) =>
559
+ Promise.resolve(searchRows({ ...query, state: "CREATED" })),
560
+ };
561
+ }
495
562
  }
496
563
 
497
564
  /**