@nanobpm/bojtos-kit 0.5.0 → 0.7.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 +55 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -1
- package/dist/session.d.ts +83 -2
- package/dist/session.js +56 -8
- package/dist/trace.d.ts +97 -0
- package/dist/trace.js +185 -0
- package/package.json +2 -2
- package/src/index.ts +37 -0
- package/src/session.ts +171 -4
- package/src/trace.ts +291 -0
package/README.md
CHANGED
|
@@ -27,6 +27,61 @@ Every command returns the post-run `Snapshot`: `activeElementIds` /
|
|
|
27
27
|
For React, use [`@nanobpm/bojtos-react`](../bojtos-react), which owns the session
|
|
28
28
|
lifecycle and reactive state on top of this kit.
|
|
29
29
|
|
|
30
|
+
## Engine variants — `lean` (default) and `readmodel`
|
|
31
|
+
|
|
32
|
+
`@nanobpm/engine-wasm` ships two binaries; a session picks one via `variant`:
|
|
33
|
+
|
|
34
|
+
- **`lean`** (default) — primary state only. Read it through `snapshot()` /
|
|
35
|
+
`events()`. Loaded statically, so every consumer bundles it.
|
|
36
|
+
- **`readmodel`** — the lean surface **plus** the gateway's Camunda-parity REST
|
|
37
|
+
read channel. Loaded via a **dynamic import**, so a lean-only page never
|
|
38
|
+
downloads the heavier read-model binary (wasm can't be tree-shaken out of a
|
|
39
|
+
single build — code-splitting is the only lever).
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import {
|
|
43
|
+
createBojtosSession,
|
|
44
|
+
type UserTaskSearchQueryResult,
|
|
45
|
+
} from "@nanobpm/bojtos-kit";
|
|
46
|
+
|
|
47
|
+
// `variant: "readmodel"` widens the return type to `ReadModelBojtosSession`:
|
|
48
|
+
const session = await createBojtosSession({ variant: "readmodel" });
|
|
49
|
+
session.deploy(bpmnXml);
|
|
50
|
+
session.createInstance("review", "{}");
|
|
51
|
+
|
|
52
|
+
// Typed against @nanobpm/engine-wasm/readmodel-types (re-exported here):
|
|
53
|
+
const open: UserTaskSearchQueryResult = session.searchUserTasks(
|
|
54
|
+
JSON.stringify({ state: "CREATED" }),
|
|
55
|
+
);
|
|
56
|
+
const form = session.getFormByKey("2251799813685250"); // FormResult | null
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The read methods — `searchUserTasks`, `searchProcessInstances`,
|
|
60
|
+
`searchVariables`, `getFormByKey`, `getResourceByKey` — return DTOs re-exported
|
|
61
|
+
from `@nanobpm/engine-wasm/readmodel-types`, which are **derived** from the
|
|
62
|
+
Camunda-parity REST OpenAPI (one source of truth, not a hand-copy).
|
|
63
|
+
|
|
64
|
+
## Trace model
|
|
65
|
+
|
|
66
|
+
The kit also holds the framework-agnostic **trace model** the shared
|
|
67
|
+
`<TraceTimeline>` (in `@nanobpm/bojtos-react`) renders — one normalized
|
|
68
|
+
row/turn-group model plus the two adapters that map a source into it, so the two
|
|
69
|
+
formerly forked timelines share one fold instead of drifting apart:
|
|
70
|
+
|
|
71
|
+
- **`foldEngineEvents(events)`** — the engine-event fold: a `WasmEvent[]` (from
|
|
72
|
+
`session.events()` / `useBojtos().events`) → normalized `TraceRow[]`, keeping the
|
|
73
|
+
run's milestones and dropping low-signal lifecycle noise. The non-agentic /
|
|
74
|
+
test-view case.
|
|
75
|
+
- **`traceEntriesToRows(entries)`** — the handler-emitted adapter: agent/tool/turn
|
|
76
|
+
`TraceEntry` lines (with the additive `turn` grouping field) → `TraceRow[]`. The
|
|
77
|
+
agentic web-demo case.
|
|
78
|
+
- **`buildTraceItems(rows)`** — folds consecutive same-`turn` rows into
|
|
79
|
+
`TraceTurnGroup`s; rows with no `turn` stay flat. `isTraceTurnGroup` narrows an
|
|
80
|
+
item. This is the grouping the view consumes.
|
|
81
|
+
|
|
82
|
+
It is pure and React-free (no React import in the kit), keeping the presentational
|
|
83
|
+
layer thin.
|
|
84
|
+
|
|
30
85
|
## Build
|
|
31
86
|
|
|
32
87
|
`dist/` (the tsc-emitted JS + `.d.ts`) is what ships, built by `prepack` on
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
export { ensureWasm, createBojtosSession, type BojtosSession, type WasmSource, } from "./session.js";
|
|
1
|
+
export { ensureWasm, ensureReadModelWasm, createBojtosSession, type BojtosSession, type ReadModelBojtosSession, type EngineVariant, type WasmSource, } from "./session.js";
|
|
2
2
|
export { dispatchWorkers, dispatchRound, settleReason, unhandledJobTypes, JobFailure, type JobHandler, type JobResult, type AgentHandler, type DispatchOptions, type DispatchResult, type RoundResult, type SettleReason, } from "./worker.js";
|
|
3
|
+
export { buildTraceItems, isTraceTurnGroup, foldEngineEvents, traceEntriesToRows, } from "./trace.js";
|
|
4
|
+
export type { TraceRowKind, TraceEntry, TraceRow, TraceTurnGroup, TraceItem, TraceAdapter, } from "./trace.js";
|
|
3
5
|
export type { Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, UserTaskDto, MessageSubscriptionDto, SignalSubscriptionDto, ElementStatDto, SequenceFlowDto, DecisionInstanceDto, ActiveEl, ActivateInstruction, AgentActivation, AgentResult, WasmEvent, } from "./types.js";
|
|
6
|
+
export type { UserTaskSearchQueryResult, UserTaskResult, ProcessInstanceSearchQueryResult, ProcessInstanceResult, VariableSearchQueryResult, VariableResult, FormResult, ResourceResult, SearchQueryResponse, SearchQueryPageResponse, } from "@nanobpm/engine-wasm/readmodel-types";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// @nanobpm/bojtos-kit — the framework-agnostic core of the Bojtos demo
|
|
2
2
|
// framework (ADR 0043). Wraps the in-browser wasm engine as a single scenario
|
|
3
3
|
// runner and re-exports the engine's snapshot/event contract types.
|
|
4
|
-
export { ensureWasm, createBojtosSession, } from "./session.js";
|
|
4
|
+
export { ensureWasm, ensureReadModelWasm, createBojtosSession, } from "./session.js";
|
|
5
5
|
export { dispatchWorkers, dispatchRound, settleReason, unhandledJobTypes, JobFailure, } from "./worker.js";
|
|
6
|
+
// The shared trace model + both adapters (engine-event fold and handler-emitted
|
|
7
|
+
// `TraceEntry`) that retired the two forked `TraceTimeline` copies (#9). Pure and
|
|
8
|
+
// React-free — the presentational component lives in @nanobpm/bojtos-react.
|
|
9
|
+
export { buildTraceItems, isTraceTurnGroup, foldEngineEvents, traceEntriesToRows, } from "./trace.js";
|
package/dist/session.d.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { type InitInput } from "@nanobpm/engine-wasm";
|
|
2
|
+
import type { FormResult, ProcessInstanceSearchQueryResult, ResourceResult, UserTaskSearchQueryResult, VariableSearchQueryResult } from "@nanobpm/engine-wasm/readmodel-types";
|
|
2
3
|
import type { ActivatedJob, ActivateInstruction, AgentResult, Snapshot, WasmEvent } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Which engine binary backs a session. The two are separate wasm builds
|
|
6
|
+
* (engine-wasm ships them at distinct subpaths, ADR 0043 §3 / engine-wasm
|
|
7
|
+
* README):
|
|
8
|
+
*
|
|
9
|
+
* - `"lean"` (default) — primary state only; the binary demos/the modeler use.
|
|
10
|
+
* Loaded via the static `@nanobpm/engine-wasm` import, so a bundler emits it
|
|
11
|
+
* for every bojtos-kit consumer.
|
|
12
|
+
* - `"readmodel"` — the lean surface **plus** the gateway's Camunda-parity REST
|
|
13
|
+
* read channel (`searchUserTasks`/`searchProcessInstances`/`searchVariables`/
|
|
14
|
+
* `getFormByKey`/`getResourceByKey`). It carries an in-memory wasm SQLite read
|
|
15
|
+
* model (~2× the wire size), so it is loaded via a **dynamic import** — a
|
|
16
|
+
* lean-only page never bundles it (wasm can't be tree-shaken out of a fat
|
|
17
|
+
* build; code-splitting is the only lever).
|
|
18
|
+
*/
|
|
19
|
+
export type EngineVariant = "lean" | "readmodel";
|
|
20
|
+
type ReadModelModule = typeof import("@nanobpm/engine-wasm/readmodel");
|
|
3
21
|
/**
|
|
4
22
|
* The source of the engine wasm binary. Under a bundler that understands
|
|
5
23
|
* `new URL(..., import.meta.url)` (e.g. Vite) the default loader needs no
|
|
@@ -20,6 +38,15 @@ export type WasmSource = InitInput;
|
|
|
20
38
|
* the binary — can retry rather than being stuck on the first rejection.
|
|
21
39
|
*/
|
|
22
40
|
export declare function ensureWasm(source?: WasmSource): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Load **and** initialise the read-model engine variant (idempotent; once per
|
|
43
|
+
* page). Unlike {@link ensureWasm} this also code-splits the binary in via a
|
|
44
|
+
* dynamic `import("@nanobpm/engine-wasm/readmodel")`, so a page that only ever
|
|
45
|
+
* calls {@link ensureWasm} never downloads the heavier read-model wasm. Same
|
|
46
|
+
* first-call-wins / retry-on-failure semantics as {@link ensureWasm}. Returns
|
|
47
|
+
* the module namespace so the caller can construct its `TestEngine`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function ensureReadModelWasm(source?: WasmSource): Promise<ReadModelModule>;
|
|
23
50
|
/**
|
|
24
51
|
* A headless handle to one in-browser engine instance: deploy a diagram, start
|
|
25
52
|
* instances, complete/fail jobs, advance the virtual clock, and read the event
|
|
@@ -141,12 +168,66 @@ export interface BojtosSession {
|
|
|
141
168
|
free(): void;
|
|
142
169
|
}
|
|
143
170
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
171
|
+
* A {@link BojtosSession} backed by the **read-model** engine variant: the full
|
|
172
|
+
* lean command surface **plus** the gateway's Camunda-parity REST read channel.
|
|
173
|
+
* Each read method delegates to the in-memory read model (kept current after
|
|
174
|
+
* every command, cleared by {@link BojtosSession.reset}) and returns the parsed
|
|
175
|
+
* DTO — typed against `@nanobpm/engine-wasm/readmodel-types`, which is derived
|
|
176
|
+
* from the same Camunda REST OpenAPI the wasm mirrors, so these stay in lockstep
|
|
177
|
+
* with the engine instead of being hand-copied. Obtain one via
|
|
178
|
+
* `createBojtosSession({ variant: "readmodel" })`.
|
|
179
|
+
*/
|
|
180
|
+
export interface ReadModelBojtosSession extends BojtosSession {
|
|
181
|
+
/**
|
|
182
|
+
* Search user tasks through the read model. Honours an optional `{ state? }`
|
|
183
|
+
* filter (e.g. `"CREATED"`). Mirrors `POST /user-tasks/search`.
|
|
184
|
+
*/
|
|
185
|
+
searchUserTasks(filterJson?: string): UserTaskSearchQueryResult;
|
|
186
|
+
/**
|
|
187
|
+
* Search process instances through the read model. Body is shape-validated;
|
|
188
|
+
* filter/sort/page fields are not yet honoured (returns every instance).
|
|
189
|
+
* Mirrors `POST /process-instances/search`.
|
|
190
|
+
*/
|
|
191
|
+
searchProcessInstances(filterJson?: string): ProcessInstanceSearchQueryResult;
|
|
192
|
+
/**
|
|
193
|
+
* Search variables through the read model. Long values are truncated with
|
|
194
|
+
* `isTruncated: true`. Mirrors `POST /variables/search`.
|
|
195
|
+
*/
|
|
196
|
+
searchVariables(filterJson?: string): VariableSearchQueryResult;
|
|
197
|
+
/**
|
|
198
|
+
* The latest deployed form for `formKey`, or `null` if none. Mirrors
|
|
199
|
+
* `GET /forms/{formKey}`.
|
|
200
|
+
*/
|
|
201
|
+
getFormByKey(formKey: string): FormResult | null;
|
|
202
|
+
/**
|
|
203
|
+
* The generic resource for `resourceKey`, or `null` if none. Mirrors
|
|
204
|
+
* `GET /resources/{resourceKey}`.
|
|
205
|
+
*/
|
|
206
|
+
getResourceByKey(resourceKey: string): ResourceResult | null;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Create a fresh headless engine session. Ensures the chosen wasm variant is
|
|
210
|
+
* loaded (once per page), then constructs a new `TestEngine`. The virtual clock
|
|
146
211
|
* starts at 0; deploy a diagram before starting instances. Pass a `wasm` source
|
|
147
212
|
* in environments where the default `import.meta.url` loader can't resolve the
|
|
148
213
|
* binary (Node/Jest, or the external-`.wasm` mode — ADR 0043 §3).
|
|
214
|
+
*
|
|
215
|
+
* With `variant: "readmodel"` the returned session also exposes the gateway's
|
|
216
|
+
* REST read channel (typed {@link ReadModelBojtosSession}); the default `"lean"`
|
|
217
|
+
* variant is state-only and never downloads the heavier read-model binary. A
|
|
218
|
+
* statically-`"readmodel"` variant widens the return type; a value only known as
|
|
219
|
+
* the `EngineVariant` union resolves to the base {@link BojtosSession}.
|
|
149
220
|
*/
|
|
150
221
|
export declare function createBojtosSession(opts?: {
|
|
151
222
|
wasm?: WasmSource;
|
|
223
|
+
variant?: "lean";
|
|
224
|
+
}): Promise<BojtosSession>;
|
|
225
|
+
export declare function createBojtosSession(opts: {
|
|
226
|
+
wasm?: WasmSource;
|
|
227
|
+
variant: "readmodel";
|
|
228
|
+
}): Promise<ReadModelBojtosSession>;
|
|
229
|
+
export declare function createBojtosSession(opts: {
|
|
230
|
+
wasm?: WasmSource;
|
|
231
|
+
variant: EngineVariant;
|
|
152
232
|
}): Promise<BojtosSession>;
|
|
233
|
+
export {};
|
package/dist/session.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import init, { TestEngine } from "@nanobpm/engine-wasm";
|
|
2
2
|
// Lazily initialise the wasm module exactly once per page, no matter how many
|
|
3
|
-
// sessions are created. Mirrors the console's original `ensureWasm`.
|
|
3
|
+
// sessions are created. Mirrors the console's original `ensureWasm`. The two
|
|
4
|
+
// variants init independently (a page may use either or both).
|
|
4
5
|
let wasmReady = null;
|
|
6
|
+
let readModelReady = null;
|
|
5
7
|
/**
|
|
6
8
|
* Initialise the wasm engine module (idempotent; safe to call repeatedly). The
|
|
7
9
|
* first successful call wins: a `source` passed to a later call is ignored once
|
|
@@ -24,6 +26,28 @@ export function ensureWasm(source) {
|
|
|
24
26
|
}
|
|
25
27
|
return wasmReady;
|
|
26
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Load **and** initialise the read-model engine variant (idempotent; once per
|
|
31
|
+
* page). Unlike {@link ensureWasm} this also code-splits the binary in via a
|
|
32
|
+
* dynamic `import("@nanobpm/engine-wasm/readmodel")`, so a page that only ever
|
|
33
|
+
* calls {@link ensureWasm} never downloads the heavier read-model wasm. Same
|
|
34
|
+
* first-call-wins / retry-on-failure semantics as {@link ensureWasm}. Returns
|
|
35
|
+
* the module namespace so the caller can construct its `TestEngine`.
|
|
36
|
+
*/
|
|
37
|
+
export function ensureReadModelWasm(source) {
|
|
38
|
+
if (!readModelReady) {
|
|
39
|
+
readModelReady = import("@nanobpm/engine-wasm/readmodel")
|
|
40
|
+
.then(async (mod) => {
|
|
41
|
+
await mod.default(source === undefined ? undefined : { module_or_path: source });
|
|
42
|
+
return mod;
|
|
43
|
+
})
|
|
44
|
+
.catch((e) => {
|
|
45
|
+
readModelReady = null;
|
|
46
|
+
throw e;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return readModelReady;
|
|
50
|
+
}
|
|
27
51
|
function parseSnapshot(json) {
|
|
28
52
|
// The wasm engine is the schema authority; its JSON is the contract boundary.
|
|
29
53
|
return JSON.parse(json);
|
|
@@ -104,14 +128,38 @@ class WasmBojtosSession {
|
|
|
104
128
|
this.engine.free();
|
|
105
129
|
}
|
|
106
130
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
class WasmReadModelSession extends WasmBojtosSession {
|
|
132
|
+
// The read-model engine is a structural superset of the lean `TestEngine`
|
|
133
|
+
// (identical command surface + the 5 read methods), so it satisfies the base
|
|
134
|
+
// constructor while we keep our own read-model-typed reference for the read
|
|
135
|
+
// channel — no casts, so a future divergence in the shared surface is a
|
|
136
|
+
// compile error rather than a runtime one.
|
|
137
|
+
rm;
|
|
138
|
+
constructor(engine) {
|
|
139
|
+
super(engine);
|
|
140
|
+
this.rm = engine;
|
|
141
|
+
}
|
|
142
|
+
searchUserTasks(filterJson = "{}") {
|
|
143
|
+
return JSON.parse(this.rm.searchUserTasks(filterJson || "{}"));
|
|
144
|
+
}
|
|
145
|
+
searchProcessInstances(filterJson = "{}") {
|
|
146
|
+
return JSON.parse(this.rm.searchProcessInstances(filterJson || "{}"));
|
|
147
|
+
}
|
|
148
|
+
searchVariables(filterJson = "{}") {
|
|
149
|
+
return JSON.parse(this.rm.searchVariables(filterJson || "{}"));
|
|
150
|
+
}
|
|
151
|
+
getFormByKey(formKey) {
|
|
152
|
+
return JSON.parse(this.rm.getFormByKey(formKey));
|
|
153
|
+
}
|
|
154
|
+
getResourceByKey(resourceKey) {
|
|
155
|
+
return JSON.parse(this.rm.getResourceByKey(resourceKey));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
114
158
|
export async function createBojtosSession(opts) {
|
|
159
|
+
if (opts?.variant === "readmodel") {
|
|
160
|
+
const mod = await ensureReadModelWasm(opts.wasm);
|
|
161
|
+
return new WasmReadModelSession(new mod.TestEngine());
|
|
162
|
+
}
|
|
115
163
|
await ensureWasm(opts?.wasm);
|
|
116
164
|
return new WasmBojtosSession(new TestEngine());
|
|
117
165
|
}
|
package/dist/trace.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { WasmEvent } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Semantic classification of a trace row, driving how the view styles it (the
|
|
4
|
+
* `log-<kind>` class the two forked timelines already keyed off) and which
|
|
5
|
+
* affordance it carries. Framework-agnostic — an engine-event fold and an
|
|
6
|
+
* agentic handler stream both land in this shared vocabulary:
|
|
7
|
+
*
|
|
8
|
+
* - `start` — the run/instance began.
|
|
9
|
+
* - `agent` — an agent decision, or a tool the agent activated this turn.
|
|
10
|
+
* - `llm` — a raw model reply.
|
|
11
|
+
* - `tool` — a tool/handler log line (a job running, a timer, a message).
|
|
12
|
+
* - `human` — a user task awaiting or completed by a person.
|
|
13
|
+
* - `done` — the final outcome (the instance completed/terminated).
|
|
14
|
+
* - `error` — a failure, incident, or thrown error.
|
|
15
|
+
* - `vars` — a variables/result update (e.g. what a tool returned).
|
|
16
|
+
*/
|
|
17
|
+
export type TraceRowKind = "start" | "agent" | "llm" | "tool" | "human" | "done" | "error" | "vars";
|
|
18
|
+
/**
|
|
19
|
+
* A source line before it is placed in the normalized model. This is the shape a
|
|
20
|
+
* handler emits (the web-demo framework's `TraceEntry`): `kind`/`text` are all a
|
|
21
|
+
* plain consumer needs; every other field is additive and safe to ignore.
|
|
22
|
+
*/
|
|
23
|
+
export interface TraceEntry {
|
|
24
|
+
kind: TraceRowKind;
|
|
25
|
+
/** The human-readable line. */
|
|
26
|
+
text: string;
|
|
27
|
+
/**
|
|
28
|
+
* Stable id for an entry that updates in place — a streaming completion grows
|
|
29
|
+
* one line rather than spamming forty.
|
|
30
|
+
*/
|
|
31
|
+
key?: string;
|
|
32
|
+
/** True while the entry is still being produced (renders a spinner). */
|
|
33
|
+
pending?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Groups every entry produced by one agent turn together (the streamed LLM
|
|
36
|
+
* reply, each tool it activated, and that tool's result). Consecutive entries
|
|
37
|
+
* sharing a `turn` fold into one turn card; entries with no `turn` render as
|
|
38
|
+
* plain rows in their original order.
|
|
39
|
+
*/
|
|
40
|
+
turn?: number;
|
|
41
|
+
/** The BPMN element (tool or task) this entry concerns. */
|
|
42
|
+
elementId?: string;
|
|
43
|
+
/** Arguments supplied when activating a tool — the coerced values, not the raw reply. */
|
|
44
|
+
args?: Record<string, unknown>;
|
|
45
|
+
/** What a tool/handler returned, paired with its activation by `elementId`. */
|
|
46
|
+
result?: unknown;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A normalized row: a {@link TraceEntry} stamped with a stable, monotonic `id`.
|
|
50
|
+
* The `id` is what the view keys off and what pairs a tool's result with its
|
|
51
|
+
* activation and orders loose lines within a turn.
|
|
52
|
+
*/
|
|
53
|
+
export interface TraceRow extends TraceEntry {
|
|
54
|
+
id: number;
|
|
55
|
+
}
|
|
56
|
+
/** Consecutive same-turn rows, folded into one group by {@link buildTraceItems}. */
|
|
57
|
+
export interface TraceTurnGroup {
|
|
58
|
+
turn: number;
|
|
59
|
+
rows: TraceRow[];
|
|
60
|
+
}
|
|
61
|
+
/** A top-level item the view renders: either a plain row or a turn group. */
|
|
62
|
+
export type TraceItem = TraceRow | TraceTurnGroup;
|
|
63
|
+
/** Narrow a {@link TraceItem} to a {@link TraceTurnGroup}. */
|
|
64
|
+
export declare function isTraceTurnGroup(item: TraceItem): item is TraceTurnGroup;
|
|
65
|
+
/**
|
|
66
|
+
* An adapter maps a source (`WasmEvent[]`, a `TraceEntry[]`, …) into the shared
|
|
67
|
+
* normalized row model. Both built-in adapters — {@link foldEngineEvents} and
|
|
68
|
+
* {@link traceEntriesToRows} — satisfy this; a consumer can supply its own for a
|
|
69
|
+
* bespoke source.
|
|
70
|
+
*/
|
|
71
|
+
export type TraceAdapter<TSource> = (source: TSource) => TraceRow[];
|
|
72
|
+
/**
|
|
73
|
+
* Fold a flat list of normalized rows into the view model: consecutive rows
|
|
74
|
+
* sharing a `turn` become one {@link TraceTurnGroup}; everything else stays a
|
|
75
|
+
* plain row in its original order. A row with no `turn` breaks the current group,
|
|
76
|
+
* so an interleaved non-turn line never gets swallowed into a card. This is the
|
|
77
|
+
* grouping both forked timelines did by hand, lifted into the shared kit.
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildTraceItems(rows: TraceRow[]): TraceItem[];
|
|
80
|
+
/**
|
|
81
|
+
* The handler-emitted `TraceEntry` adapter: stamp each entry with a stable `id`
|
|
82
|
+
* (its index) to lift it into a {@link TraceRow}. The entries already carry the
|
|
83
|
+
* additive `turn`/`elementId`/`args`/`result` fields, so {@link buildTraceItems}
|
|
84
|
+
* over the result reproduces the agentic turn-grouped card view without any
|
|
85
|
+
* re-forked grouping logic. The input is never mutated.
|
|
86
|
+
*/
|
|
87
|
+
export declare function traceEntriesToRows(entries: readonly TraceEntry[]): TraceRow[];
|
|
88
|
+
/**
|
|
89
|
+
* The engine-event fold adapter: map a `WasmEvent[]` (the flattened
|
|
90
|
+
* `{ seq, now, type, …snake_case }` stream from `useBojtos().events`) into
|
|
91
|
+
* normalized rows, keeping only the meaningful milestones (see
|
|
92
|
+
* {@link ENGINE_EVENT_RULES}). Each row's `id` is the event's `seq`, so ids stay
|
|
93
|
+
* stable and monotonic across re-reads of a growing log. Engine events carry no
|
|
94
|
+
* turn, so {@link buildTraceItems} over the result is a flat list of rows — the
|
|
95
|
+
* non-agentic test-view shape.
|
|
96
|
+
*/
|
|
97
|
+
export declare function foldEngineEvents(events: readonly WasmEvent[]): TraceRow[];
|
package/dist/trace.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// The framework-agnostic trace model shared by the Bojtos demo framework and the
|
|
2
|
+
// console test-view — the single source that retired the two drifted, forked
|
|
3
|
+
// `TraceTimeline` copies (nanobpm/bojtos#9). It defines one normalized row/turn
|
|
4
|
+
// model plus the two adapters that map a source into it:
|
|
5
|
+
//
|
|
6
|
+
// 1. the **engine-event fold** — `WasmEvent[]` (from `useBojtos().events`) →
|
|
7
|
+
// rows, covering the non-agentic / test-view case; and
|
|
8
|
+
// 2. the **handler-emitted `TraceEntry`** adapter — the agent/tool/turn entries
|
|
9
|
+
// (with the additive `turn` grouping field) → rows, covering the agentic
|
|
10
|
+
// web-demo case.
|
|
11
|
+
//
|
|
12
|
+
// It is deliberately React-free: the presentational component (`TraceTimeline` in
|
|
13
|
+
// `@nanobpm/bojtos-react`) renders this model, keeping the view layer thin.
|
|
14
|
+
/** Narrow a {@link TraceItem} to a {@link TraceTurnGroup}. */
|
|
15
|
+
export function isTraceTurnGroup(item) {
|
|
16
|
+
return item.rows !== undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Fold a flat list of normalized rows into the view model: consecutive rows
|
|
20
|
+
* sharing a `turn` become one {@link TraceTurnGroup}; everything else stays a
|
|
21
|
+
* plain row in its original order. A row with no `turn` breaks the current group,
|
|
22
|
+
* so an interleaved non-turn line never gets swallowed into a card. This is the
|
|
23
|
+
* grouping both forked timelines did by hand, lifted into the shared kit.
|
|
24
|
+
*/
|
|
25
|
+
export function buildTraceItems(rows) {
|
|
26
|
+
const items = [];
|
|
27
|
+
let current = null;
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
if (row.turn !== undefined) {
|
|
30
|
+
if (current && current.turn === row.turn) {
|
|
31
|
+
current.rows.push(row);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
current = { turn: row.turn, rows: [row] };
|
|
35
|
+
items.push(current);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
current = null;
|
|
40
|
+
items.push(row);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return items;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The handler-emitted `TraceEntry` adapter: stamp each entry with a stable `id`
|
|
47
|
+
* (its index) to lift it into a {@link TraceRow}. The entries already carry the
|
|
48
|
+
* additive `turn`/`elementId`/`args`/`result` fields, so {@link buildTraceItems}
|
|
49
|
+
* over the result reproduces the agentic turn-grouped card view without any
|
|
50
|
+
* re-forked grouping logic. The input is never mutated.
|
|
51
|
+
*/
|
|
52
|
+
export function traceEntriesToRows(entries) {
|
|
53
|
+
return entries.map((entry, id) => ({ ...entry, id }));
|
|
54
|
+
}
|
|
55
|
+
/** Read a string field off a `WasmEvent`'s open payload, or `undefined`. */
|
|
56
|
+
function str(ev, key) {
|
|
57
|
+
const v = ev[key];
|
|
58
|
+
return typeof v === "string" ? v : undefined;
|
|
59
|
+
}
|
|
60
|
+
/** The element this event concerns, if it names one — for pairing/labelling. */
|
|
61
|
+
function elementOf(ev) {
|
|
62
|
+
return str(ev, "element_id");
|
|
63
|
+
}
|
|
64
|
+
const ENGINE_EVENT_RULES = {
|
|
65
|
+
ProcessInstanceCreated: {
|
|
66
|
+
kind: "start",
|
|
67
|
+
text: (ev) => `Process ${str(ev, "process_id") ?? "instance"} started`,
|
|
68
|
+
},
|
|
69
|
+
JobCreated: {
|
|
70
|
+
kind: "tool",
|
|
71
|
+
text: (ev) => `Job ${str(ev, "job_type") ?? ""} created`.trim() +
|
|
72
|
+
(elementOf(ev) ? ` on ${elementOf(ev)}` : ""),
|
|
73
|
+
},
|
|
74
|
+
JobCompleted: {
|
|
75
|
+
kind: "vars",
|
|
76
|
+
text: (ev) => `Job ${str(ev, "job_type") ?? ""} completed`.trim(),
|
|
77
|
+
},
|
|
78
|
+
JobFailed: {
|
|
79
|
+
kind: "error",
|
|
80
|
+
text: (ev) => `Job ${str(ev, "job_type") ?? ""} failed`.trim(),
|
|
81
|
+
},
|
|
82
|
+
JobErrorThrown: {
|
|
83
|
+
kind: "error",
|
|
84
|
+
text: (ev) => `Job threw error ${str(ev, "error_code") ?? ""}`.trim() +
|
|
85
|
+
(elementOf(ev) ? ` on ${elementOf(ev)}` : ""),
|
|
86
|
+
},
|
|
87
|
+
IncidentRaised: {
|
|
88
|
+
kind: "error",
|
|
89
|
+
text: (ev) => `Incident on ${elementOf(ev) ?? "instance"}` +
|
|
90
|
+
(str(ev, "reason") ? `: ${str(ev, "reason")}` : ""),
|
|
91
|
+
},
|
|
92
|
+
IncidentResolved: {
|
|
93
|
+
kind: "tool",
|
|
94
|
+
text: (ev) => `Incident resolved on ${elementOf(ev) ?? "instance"}`,
|
|
95
|
+
},
|
|
96
|
+
UserTaskCreated: {
|
|
97
|
+
kind: "human",
|
|
98
|
+
text: (ev) => `User task ${elementOf(ev) ?? ""} awaiting a human`.trim(),
|
|
99
|
+
},
|
|
100
|
+
UserTaskAssigned: {
|
|
101
|
+
kind: "human",
|
|
102
|
+
text: (ev) => `User task ${elementOf(ev) ?? ""} assigned`.trim() +
|
|
103
|
+
(str(ev, "assignee") ? ` to ${str(ev, "assignee")}` : ""),
|
|
104
|
+
},
|
|
105
|
+
UserTaskCompleted: {
|
|
106
|
+
kind: "human",
|
|
107
|
+
text: (ev) => `User task ${elementOf(ev) ?? ""} completed`.trim(),
|
|
108
|
+
},
|
|
109
|
+
UserTaskCanceled: {
|
|
110
|
+
kind: "human",
|
|
111
|
+
text: (ev) => `User task ${elementOf(ev) ?? ""} canceled`.trim(),
|
|
112
|
+
},
|
|
113
|
+
TimerCreated: {
|
|
114
|
+
kind: "tool",
|
|
115
|
+
text: (ev) => `Timer set on ${elementOf(ev) ?? "instance"}`,
|
|
116
|
+
},
|
|
117
|
+
TimerTriggered: {
|
|
118
|
+
kind: "tool",
|
|
119
|
+
text: (ev) => `Timer fired on ${elementOf(ev) ?? "instance"}`,
|
|
120
|
+
},
|
|
121
|
+
MessagePublished: {
|
|
122
|
+
kind: "tool",
|
|
123
|
+
text: (ev) => `Message ${str(ev, "message_name") ?? ""} published`.trim(),
|
|
124
|
+
},
|
|
125
|
+
MessageCorrelated: {
|
|
126
|
+
kind: "tool",
|
|
127
|
+
text: (ev) => `Message ${str(ev, "message_name") ?? ""} correlated`.trim(),
|
|
128
|
+
},
|
|
129
|
+
SignalBroadcast: {
|
|
130
|
+
kind: "tool",
|
|
131
|
+
text: (ev) => `Signal ${str(ev, "signal_name") ?? ""} broadcast`.trim(),
|
|
132
|
+
},
|
|
133
|
+
SignalCorrelated: {
|
|
134
|
+
kind: "tool",
|
|
135
|
+
text: (ev) => `Signal ${str(ev, "signal_name") ?? ""} correlated`.trim(),
|
|
136
|
+
},
|
|
137
|
+
AdHocActivated: {
|
|
138
|
+
kind: "agent",
|
|
139
|
+
text: (ev) => `Ad-hoc sub-process ${elementOf(ev) ?? ""} activated`.trim(),
|
|
140
|
+
},
|
|
141
|
+
AdHocToolActivated: {
|
|
142
|
+
kind: "agent",
|
|
143
|
+
text: (ev) => `Tool ${elementOf(ev) ?? ""} activated`.trim(),
|
|
144
|
+
},
|
|
145
|
+
AdHocToolCompleted: {
|
|
146
|
+
kind: "vars",
|
|
147
|
+
text: (ev) => `Tool ${elementOf(ev) ?? ""} returned`.trim(),
|
|
148
|
+
},
|
|
149
|
+
AdHocCompleted: {
|
|
150
|
+
kind: "agent",
|
|
151
|
+
text: (ev) => `Ad-hoc sub-process ${elementOf(ev) ?? ""} completed`.trim(),
|
|
152
|
+
},
|
|
153
|
+
ProcessInstanceCompleted: {
|
|
154
|
+
kind: "done",
|
|
155
|
+
text: () => "Process completed",
|
|
156
|
+
},
|
|
157
|
+
ProcessInstanceTerminated: {
|
|
158
|
+
kind: "error",
|
|
159
|
+
text: () => "Process terminated",
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* The engine-event fold adapter: map a `WasmEvent[]` (the flattened
|
|
164
|
+
* `{ seq, now, type, …snake_case }` stream from `useBojtos().events`) into
|
|
165
|
+
* normalized rows, keeping only the meaningful milestones (see
|
|
166
|
+
* {@link ENGINE_EVENT_RULES}). Each row's `id` is the event's `seq`, so ids stay
|
|
167
|
+
* stable and monotonic across re-reads of a growing log. Engine events carry no
|
|
168
|
+
* turn, so {@link buildTraceItems} over the result is a flat list of rows — the
|
|
169
|
+
* non-agentic test-view shape.
|
|
170
|
+
*/
|
|
171
|
+
export function foldEngineEvents(events) {
|
|
172
|
+
const rows = [];
|
|
173
|
+
for (const ev of events) {
|
|
174
|
+
const rule = ENGINE_EVENT_RULES[ev.type];
|
|
175
|
+
if (!rule)
|
|
176
|
+
continue;
|
|
177
|
+
rows.push({
|
|
178
|
+
id: ev.seq,
|
|
179
|
+
kind: rule.kind,
|
|
180
|
+
text: rule.text(ev),
|
|
181
|
+
elementId: elementOf(ev),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return rows;
|
|
185
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/bojtos-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"prepack": "npm run build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@nanobpm/engine-wasm": "^0.
|
|
35
|
+
"@nanobpm/engine-wasm": "^0.7.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"typescript": "^5.6.3"
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
ensureWasm,
|
|
7
|
+
ensureReadModelWasm,
|
|
7
8
|
createBojtosSession,
|
|
8
9
|
type BojtosSession,
|
|
10
|
+
type ReadModelBojtosSession,
|
|
11
|
+
type EngineVariant,
|
|
9
12
|
type WasmSource,
|
|
10
13
|
} from "./session.js";
|
|
11
14
|
export {
|
|
@@ -22,6 +25,23 @@ export {
|
|
|
22
25
|
type RoundResult,
|
|
23
26
|
type SettleReason,
|
|
24
27
|
} from "./worker.js";
|
|
28
|
+
// The shared trace model + both adapters (engine-event fold and handler-emitted
|
|
29
|
+
// `TraceEntry`) that retired the two forked `TraceTimeline` copies (#9). Pure and
|
|
30
|
+
// React-free — the presentational component lives in @nanobpm/bojtos-react.
|
|
31
|
+
export {
|
|
32
|
+
buildTraceItems,
|
|
33
|
+
isTraceTurnGroup,
|
|
34
|
+
foldEngineEvents,
|
|
35
|
+
traceEntriesToRows,
|
|
36
|
+
} from "./trace.js";
|
|
37
|
+
export type {
|
|
38
|
+
TraceRowKind,
|
|
39
|
+
TraceEntry,
|
|
40
|
+
TraceRow,
|
|
41
|
+
TraceTurnGroup,
|
|
42
|
+
TraceItem,
|
|
43
|
+
TraceAdapter,
|
|
44
|
+
} from "./trace.js";
|
|
25
45
|
// Every type reachable from `Snapshot` is exported: a consumer that can read
|
|
26
46
|
// `snapshot.userTasks` must also be able to name `UserTaskDto` to write a
|
|
27
47
|
// helper for it. Keep this list exhaustive when adding to `types.ts`.
|
|
@@ -44,3 +64,20 @@ export type {
|
|
|
44
64
|
AgentResult,
|
|
45
65
|
WasmEvent,
|
|
46
66
|
} from "./types.js";
|
|
67
|
+
// The read-model query-result DTOs, re-exported from
|
|
68
|
+
// `@nanobpm/engine-wasm/readmodel-types` (derived from the Camunda-parity REST
|
|
69
|
+
// OpenAPI — a single source of truth, not a hand-copy). A consumer that reads a
|
|
70
|
+
// `ReadModelBojtosSession`'s `searchUserTasks()` / `getFormByKey()` return must
|
|
71
|
+
// be able to name these to write helpers over them.
|
|
72
|
+
export type {
|
|
73
|
+
UserTaskSearchQueryResult,
|
|
74
|
+
UserTaskResult,
|
|
75
|
+
ProcessInstanceSearchQueryResult,
|
|
76
|
+
ProcessInstanceResult,
|
|
77
|
+
VariableSearchQueryResult,
|
|
78
|
+
VariableResult,
|
|
79
|
+
FormResult,
|
|
80
|
+
ResourceResult,
|
|
81
|
+
SearchQueryResponse,
|
|
82
|
+
SearchQueryPageResponse,
|
|
83
|
+
} from "@nanobpm/engine-wasm/readmodel-types";
|
package/src/session.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import init, { type InitInput, TestEngine } from "@nanobpm/engine-wasm";
|
|
2
|
+
import type {
|
|
3
|
+
FormResult,
|
|
4
|
+
ProcessInstanceSearchQueryResult,
|
|
5
|
+
ResourceResult,
|
|
6
|
+
UserTaskSearchQueryResult,
|
|
7
|
+
VariableSearchQueryResult,
|
|
8
|
+
} from "@nanobpm/engine-wasm/readmodel-types";
|
|
2
9
|
import type {
|
|
3
10
|
ActivatedJob,
|
|
4
11
|
ActivateInstruction,
|
|
@@ -7,9 +14,35 @@ import type {
|
|
|
7
14
|
WasmEvent,
|
|
8
15
|
} from "./types.js";
|
|
9
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Which engine binary backs a session. The two are separate wasm builds
|
|
19
|
+
* (engine-wasm ships them at distinct subpaths, ADR 0043 §3 / engine-wasm
|
|
20
|
+
* README):
|
|
21
|
+
*
|
|
22
|
+
* - `"lean"` (default) — primary state only; the binary demos/the modeler use.
|
|
23
|
+
* Loaded via the static `@nanobpm/engine-wasm` import, so a bundler emits it
|
|
24
|
+
* for every bojtos-kit consumer.
|
|
25
|
+
* - `"readmodel"` — the lean surface **plus** the gateway's Camunda-parity REST
|
|
26
|
+
* read channel (`searchUserTasks`/`searchProcessInstances`/`searchVariables`/
|
|
27
|
+
* `getFormByKey`/`getResourceByKey`). It carries an in-memory wasm SQLite read
|
|
28
|
+
* model (~2× the wire size), so it is loaded via a **dynamic import** — a
|
|
29
|
+
* lean-only page never bundles it (wasm can't be tree-shaken out of a fat
|
|
30
|
+
* build; code-splitting is the only lever).
|
|
31
|
+
*/
|
|
32
|
+
export type EngineVariant = "lean" | "readmodel";
|
|
33
|
+
|
|
34
|
+
// Type-only view of the read-model module so we can name its `TestEngine`
|
|
35
|
+
// (a distinct wasm-bindgen class from lean's, with the +5 read methods) without
|
|
36
|
+
// statically importing the heavy binary — the runtime handle is fetched lazily
|
|
37
|
+
// by `ensureReadModelWasm`'s dynamic `import()`.
|
|
38
|
+
type ReadModelModule = typeof import("@nanobpm/engine-wasm/readmodel");
|
|
39
|
+
type ReadModelEngine = InstanceType<ReadModelModule["TestEngine"]>;
|
|
40
|
+
|
|
10
41
|
// Lazily initialise the wasm module exactly once per page, no matter how many
|
|
11
|
-
// sessions are created. Mirrors the console's original `ensureWasm`.
|
|
42
|
+
// sessions are created. Mirrors the console's original `ensureWasm`. The two
|
|
43
|
+
// variants init independently (a page may use either or both).
|
|
12
44
|
let wasmReady: Promise<void> | null = null;
|
|
45
|
+
let readModelReady: Promise<ReadModelModule> | null = null;
|
|
13
46
|
|
|
14
47
|
/**
|
|
15
48
|
* The source of the engine wasm binary. Under a bundler that understands
|
|
@@ -45,6 +78,33 @@ export function ensureWasm(source?: WasmSource): Promise<void> {
|
|
|
45
78
|
return wasmReady;
|
|
46
79
|
}
|
|
47
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Load **and** initialise the read-model engine variant (idempotent; once per
|
|
83
|
+
* page). Unlike {@link ensureWasm} this also code-splits the binary in via a
|
|
84
|
+
* dynamic `import("@nanobpm/engine-wasm/readmodel")`, so a page that only ever
|
|
85
|
+
* calls {@link ensureWasm} never downloads the heavier read-model wasm. Same
|
|
86
|
+
* first-call-wins / retry-on-failure semantics as {@link ensureWasm}. Returns
|
|
87
|
+
* the module namespace so the caller can construct its `TestEngine`.
|
|
88
|
+
*/
|
|
89
|
+
export function ensureReadModelWasm(
|
|
90
|
+
source?: WasmSource,
|
|
91
|
+
): Promise<ReadModelModule> {
|
|
92
|
+
if (!readModelReady) {
|
|
93
|
+
readModelReady = import("@nanobpm/engine-wasm/readmodel")
|
|
94
|
+
.then(async (mod) => {
|
|
95
|
+
await mod.default(
|
|
96
|
+
source === undefined ? undefined : { module_or_path: source },
|
|
97
|
+
);
|
|
98
|
+
return mod;
|
|
99
|
+
})
|
|
100
|
+
.catch((e) => {
|
|
101
|
+
readModelReady = null;
|
|
102
|
+
throw e;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return readModelReady;
|
|
106
|
+
}
|
|
107
|
+
|
|
48
108
|
/**
|
|
49
109
|
* A headless handle to one in-browser engine instance: deploy a diagram, start
|
|
50
110
|
* instances, complete/fail jobs, advance the virtual clock, and read the event
|
|
@@ -185,13 +245,52 @@ export interface BojtosSession {
|
|
|
185
245
|
free(): void;
|
|
186
246
|
}
|
|
187
247
|
|
|
248
|
+
/**
|
|
249
|
+
* A {@link BojtosSession} backed by the **read-model** engine variant: the full
|
|
250
|
+
* lean command surface **plus** the gateway's Camunda-parity REST read channel.
|
|
251
|
+
* Each read method delegates to the in-memory read model (kept current after
|
|
252
|
+
* every command, cleared by {@link BojtosSession.reset}) and returns the parsed
|
|
253
|
+
* DTO — typed against `@nanobpm/engine-wasm/readmodel-types`, which is derived
|
|
254
|
+
* from the same Camunda REST OpenAPI the wasm mirrors, so these stay in lockstep
|
|
255
|
+
* with the engine instead of being hand-copied. Obtain one via
|
|
256
|
+
* `createBojtosSession({ variant: "readmodel" })`.
|
|
257
|
+
*/
|
|
258
|
+
export interface ReadModelBojtosSession extends BojtosSession {
|
|
259
|
+
/**
|
|
260
|
+
* Search user tasks through the read model. Honours an optional `{ state? }`
|
|
261
|
+
* filter (e.g. `"CREATED"`). Mirrors `POST /user-tasks/search`.
|
|
262
|
+
*/
|
|
263
|
+
searchUserTasks(filterJson?: string): UserTaskSearchQueryResult;
|
|
264
|
+
/**
|
|
265
|
+
* Search process instances through the read model. Body is shape-validated;
|
|
266
|
+
* filter/sort/page fields are not yet honoured (returns every instance).
|
|
267
|
+
* Mirrors `POST /process-instances/search`.
|
|
268
|
+
*/
|
|
269
|
+
searchProcessInstances(filterJson?: string): ProcessInstanceSearchQueryResult;
|
|
270
|
+
/**
|
|
271
|
+
* Search variables through the read model. Long values are truncated with
|
|
272
|
+
* `isTruncated: true`. Mirrors `POST /variables/search`.
|
|
273
|
+
*/
|
|
274
|
+
searchVariables(filterJson?: string): VariableSearchQueryResult;
|
|
275
|
+
/**
|
|
276
|
+
* The latest deployed form for `formKey`, or `null` if none. Mirrors
|
|
277
|
+
* `GET /forms/{formKey}`.
|
|
278
|
+
*/
|
|
279
|
+
getFormByKey(formKey: string): FormResult | null;
|
|
280
|
+
/**
|
|
281
|
+
* The generic resource for `resourceKey`, or `null` if none. Mirrors
|
|
282
|
+
* `GET /resources/{resourceKey}`.
|
|
283
|
+
*/
|
|
284
|
+
getResourceByKey(resourceKey: string): ResourceResult | null;
|
|
285
|
+
}
|
|
286
|
+
|
|
188
287
|
function parseSnapshot(json: string): Snapshot {
|
|
189
288
|
// The wasm engine is the schema authority; its JSON is the contract boundary.
|
|
190
289
|
return JSON.parse(json) as Snapshot;
|
|
191
290
|
}
|
|
192
291
|
|
|
193
292
|
class WasmBojtosSession implements BojtosSession {
|
|
194
|
-
|
|
293
|
+
protected readonly engine: TestEngine;
|
|
195
294
|
|
|
196
295
|
constructor(engine: TestEngine) {
|
|
197
296
|
this.engine = engine;
|
|
@@ -350,16 +449,84 @@ class WasmBojtosSession implements BojtosSession {
|
|
|
350
449
|
}
|
|
351
450
|
}
|
|
352
451
|
|
|
452
|
+
class WasmReadModelSession
|
|
453
|
+
extends WasmBojtosSession
|
|
454
|
+
implements ReadModelBojtosSession
|
|
455
|
+
{
|
|
456
|
+
// The read-model engine is a structural superset of the lean `TestEngine`
|
|
457
|
+
// (identical command surface + the 5 read methods), so it satisfies the base
|
|
458
|
+
// constructor while we keep our own read-model-typed reference for the read
|
|
459
|
+
// channel — no casts, so a future divergence in the shared surface is a
|
|
460
|
+
// compile error rather than a runtime one.
|
|
461
|
+
private readonly rm: ReadModelEngine;
|
|
462
|
+
|
|
463
|
+
constructor(engine: ReadModelEngine) {
|
|
464
|
+
super(engine);
|
|
465
|
+
this.rm = engine;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
searchUserTasks(filterJson = "{}"): UserTaskSearchQueryResult {
|
|
469
|
+
return JSON.parse(
|
|
470
|
+
this.rm.searchUserTasks(filterJson || "{}"),
|
|
471
|
+
) as UserTaskSearchQueryResult;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
searchProcessInstances(filterJson = "{}"): ProcessInstanceSearchQueryResult {
|
|
475
|
+
return JSON.parse(
|
|
476
|
+
this.rm.searchProcessInstances(filterJson || "{}"),
|
|
477
|
+
) as ProcessInstanceSearchQueryResult;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
searchVariables(filterJson = "{}"): VariableSearchQueryResult {
|
|
481
|
+
return JSON.parse(
|
|
482
|
+
this.rm.searchVariables(filterJson || "{}"),
|
|
483
|
+
) as VariableSearchQueryResult;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
getFormByKey(formKey: string): FormResult | null {
|
|
487
|
+
return JSON.parse(this.rm.getFormByKey(formKey)) as FormResult | null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
getResourceByKey(resourceKey: string): ResourceResult | null {
|
|
491
|
+
return JSON.parse(
|
|
492
|
+
this.rm.getResourceByKey(resourceKey),
|
|
493
|
+
) as ResourceResult | null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
353
497
|
/**
|
|
354
|
-
* Create a fresh headless engine session. Ensures the wasm
|
|
355
|
-
* (once per page), then constructs a new
|
|
498
|
+
* Create a fresh headless engine session. Ensures the chosen wasm variant is
|
|
499
|
+
* loaded (once per page), then constructs a new `TestEngine`. The virtual clock
|
|
356
500
|
* starts at 0; deploy a diagram before starting instances. Pass a `wasm` source
|
|
357
501
|
* in environments where the default `import.meta.url` loader can't resolve the
|
|
358
502
|
* binary (Node/Jest, or the external-`.wasm` mode — ADR 0043 §3).
|
|
503
|
+
*
|
|
504
|
+
* With `variant: "readmodel"` the returned session also exposes the gateway's
|
|
505
|
+
* REST read channel (typed {@link ReadModelBojtosSession}); the default `"lean"`
|
|
506
|
+
* variant is state-only and never downloads the heavier read-model binary. A
|
|
507
|
+
* statically-`"readmodel"` variant widens the return type; a value only known as
|
|
508
|
+
* the `EngineVariant` union resolves to the base {@link BojtosSession}.
|
|
359
509
|
*/
|
|
360
510
|
export async function createBojtosSession(opts?: {
|
|
361
511
|
wasm?: WasmSource;
|
|
512
|
+
variant?: "lean";
|
|
513
|
+
}): Promise<BojtosSession>;
|
|
514
|
+
export async function createBojtosSession(opts: {
|
|
515
|
+
wasm?: WasmSource;
|
|
516
|
+
variant: "readmodel";
|
|
517
|
+
}): Promise<ReadModelBojtosSession>;
|
|
518
|
+
export async function createBojtosSession(opts: {
|
|
519
|
+
wasm?: WasmSource;
|
|
520
|
+
variant: EngineVariant;
|
|
521
|
+
}): Promise<BojtosSession>;
|
|
522
|
+
export async function createBojtosSession(opts?: {
|
|
523
|
+
wasm?: WasmSource;
|
|
524
|
+
variant?: EngineVariant;
|
|
362
525
|
}): Promise<BojtosSession> {
|
|
526
|
+
if (opts?.variant === "readmodel") {
|
|
527
|
+
const mod = await ensureReadModelWasm(opts.wasm);
|
|
528
|
+
return new WasmReadModelSession(new mod.TestEngine());
|
|
529
|
+
}
|
|
363
530
|
await ensureWasm(opts?.wasm);
|
|
364
531
|
return new WasmBojtosSession(new TestEngine());
|
|
365
532
|
}
|
package/src/trace.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// The framework-agnostic trace model shared by the Bojtos demo framework and the
|
|
2
|
+
// console test-view — the single source that retired the two drifted, forked
|
|
3
|
+
// `TraceTimeline` copies (nanobpm/bojtos#9). It defines one normalized row/turn
|
|
4
|
+
// model plus the two adapters that map a source into it:
|
|
5
|
+
//
|
|
6
|
+
// 1. the **engine-event fold** — `WasmEvent[]` (from `useBojtos().events`) →
|
|
7
|
+
// rows, covering the non-agentic / test-view case; and
|
|
8
|
+
// 2. the **handler-emitted `TraceEntry`** adapter — the agent/tool/turn entries
|
|
9
|
+
// (with the additive `turn` grouping field) → rows, covering the agentic
|
|
10
|
+
// web-demo case.
|
|
11
|
+
//
|
|
12
|
+
// It is deliberately React-free: the presentational component (`TraceTimeline` in
|
|
13
|
+
// `@nanobpm/bojtos-react`) renders this model, keeping the view layer thin.
|
|
14
|
+
|
|
15
|
+
import type { WasmEvent } from "./types.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Semantic classification of a trace row, driving how the view styles it (the
|
|
19
|
+
* `log-<kind>` class the two forked timelines already keyed off) and which
|
|
20
|
+
* affordance it carries. Framework-agnostic — an engine-event fold and an
|
|
21
|
+
* agentic handler stream both land in this shared vocabulary:
|
|
22
|
+
*
|
|
23
|
+
* - `start` — the run/instance began.
|
|
24
|
+
* - `agent` — an agent decision, or a tool the agent activated this turn.
|
|
25
|
+
* - `llm` — a raw model reply.
|
|
26
|
+
* - `tool` — a tool/handler log line (a job running, a timer, a message).
|
|
27
|
+
* - `human` — a user task awaiting or completed by a person.
|
|
28
|
+
* - `done` — the final outcome (the instance completed/terminated).
|
|
29
|
+
* - `error` — a failure, incident, or thrown error.
|
|
30
|
+
* - `vars` — a variables/result update (e.g. what a tool returned).
|
|
31
|
+
*/
|
|
32
|
+
export type TraceRowKind =
|
|
33
|
+
| "start"
|
|
34
|
+
| "agent"
|
|
35
|
+
| "llm"
|
|
36
|
+
| "tool"
|
|
37
|
+
| "human"
|
|
38
|
+
| "done"
|
|
39
|
+
| "error"
|
|
40
|
+
| "vars";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A source line before it is placed in the normalized model. This is the shape a
|
|
44
|
+
* handler emits (the web-demo framework's `TraceEntry`): `kind`/`text` are all a
|
|
45
|
+
* plain consumer needs; every other field is additive and safe to ignore.
|
|
46
|
+
*/
|
|
47
|
+
export interface TraceEntry {
|
|
48
|
+
kind: TraceRowKind;
|
|
49
|
+
/** The human-readable line. */
|
|
50
|
+
text: string;
|
|
51
|
+
/**
|
|
52
|
+
* Stable id for an entry that updates in place — a streaming completion grows
|
|
53
|
+
* one line rather than spamming forty.
|
|
54
|
+
*/
|
|
55
|
+
key?: string;
|
|
56
|
+
/** True while the entry is still being produced (renders a spinner). */
|
|
57
|
+
pending?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Groups every entry produced by one agent turn together (the streamed LLM
|
|
60
|
+
* reply, each tool it activated, and that tool's result). Consecutive entries
|
|
61
|
+
* sharing a `turn` fold into one turn card; entries with no `turn` render as
|
|
62
|
+
* plain rows in their original order.
|
|
63
|
+
*/
|
|
64
|
+
turn?: number;
|
|
65
|
+
/** The BPMN element (tool or task) this entry concerns. */
|
|
66
|
+
elementId?: string;
|
|
67
|
+
/** Arguments supplied when activating a tool — the coerced values, not the raw reply. */
|
|
68
|
+
args?: Record<string, unknown>;
|
|
69
|
+
/** What a tool/handler returned, paired with its activation by `elementId`. */
|
|
70
|
+
result?: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A normalized row: a {@link TraceEntry} stamped with a stable, monotonic `id`.
|
|
75
|
+
* The `id` is what the view keys off and what pairs a tool's result with its
|
|
76
|
+
* activation and orders loose lines within a turn.
|
|
77
|
+
*/
|
|
78
|
+
export interface TraceRow extends TraceEntry {
|
|
79
|
+
id: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Consecutive same-turn rows, folded into one group by {@link buildTraceItems}. */
|
|
83
|
+
export interface TraceTurnGroup {
|
|
84
|
+
turn: number;
|
|
85
|
+
rows: TraceRow[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A top-level item the view renders: either a plain row or a turn group. */
|
|
89
|
+
export type TraceItem = TraceRow | TraceTurnGroup;
|
|
90
|
+
|
|
91
|
+
/** Narrow a {@link TraceItem} to a {@link TraceTurnGroup}. */
|
|
92
|
+
export function isTraceTurnGroup(item: TraceItem): item is TraceTurnGroup {
|
|
93
|
+
return (item as TraceTurnGroup).rows !== undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* An adapter maps a source (`WasmEvent[]`, a `TraceEntry[]`, …) into the shared
|
|
98
|
+
* normalized row model. Both built-in adapters — {@link foldEngineEvents} and
|
|
99
|
+
* {@link traceEntriesToRows} — satisfy this; a consumer can supply its own for a
|
|
100
|
+
* bespoke source.
|
|
101
|
+
*/
|
|
102
|
+
export type TraceAdapter<TSource> = (source: TSource) => TraceRow[];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fold a flat list of normalized rows into the view model: consecutive rows
|
|
106
|
+
* sharing a `turn` become one {@link TraceTurnGroup}; everything else stays a
|
|
107
|
+
* plain row in its original order. A row with no `turn` breaks the current group,
|
|
108
|
+
* so an interleaved non-turn line never gets swallowed into a card. This is the
|
|
109
|
+
* grouping both forked timelines did by hand, lifted into the shared kit.
|
|
110
|
+
*/
|
|
111
|
+
export function buildTraceItems(rows: TraceRow[]): TraceItem[] {
|
|
112
|
+
const items: TraceItem[] = [];
|
|
113
|
+
let current: TraceTurnGroup | null = null;
|
|
114
|
+
for (const row of rows) {
|
|
115
|
+
if (row.turn !== undefined) {
|
|
116
|
+
if (current && current.turn === row.turn) {
|
|
117
|
+
current.rows.push(row);
|
|
118
|
+
} else {
|
|
119
|
+
current = { turn: row.turn, rows: [row] };
|
|
120
|
+
items.push(current);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
current = null;
|
|
124
|
+
items.push(row);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return items;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The handler-emitted `TraceEntry` adapter: stamp each entry with a stable `id`
|
|
132
|
+
* (its index) to lift it into a {@link TraceRow}. The entries already carry the
|
|
133
|
+
* additive `turn`/`elementId`/`args`/`result` fields, so {@link buildTraceItems}
|
|
134
|
+
* over the result reproduces the agentic turn-grouped card view without any
|
|
135
|
+
* re-forked grouping logic. The input is never mutated.
|
|
136
|
+
*/
|
|
137
|
+
export function traceEntriesToRows(entries: readonly TraceEntry[]): TraceRow[] {
|
|
138
|
+
return entries.map((entry, id) => ({ ...entry, id }));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* How one engine event type folds into the trace: its row {@link TraceRowKind}
|
|
143
|
+
* and a function turning the event's snake_case payload into a line. Returning
|
|
144
|
+
* a mapping opts the event into the story; every type absent from the table
|
|
145
|
+
* below is deliberately dropped as low-signal lifecycle noise (`ElementActivating`
|
|
146
|
+
* / `ElementCompleting`, `JobActivated`, `SequenceFlowTaken`, the scoped-variable
|
|
147
|
+
* and parallel-join bookkeeping), so the fold reads as a run's milestones rather
|
|
148
|
+
* than a raw trace.
|
|
149
|
+
*/
|
|
150
|
+
interface EngineEventRule {
|
|
151
|
+
kind: TraceRowKind;
|
|
152
|
+
text: (ev: WasmEvent) => string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Read a string field off a `WasmEvent`'s open payload, or `undefined`. */
|
|
156
|
+
function str(ev: WasmEvent, key: string): string | undefined {
|
|
157
|
+
const v = ev[key];
|
|
158
|
+
return typeof v === "string" ? v : undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The element this event concerns, if it names one — for pairing/labelling. */
|
|
162
|
+
function elementOf(ev: WasmEvent): string | undefined {
|
|
163
|
+
return str(ev, "element_id");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const ENGINE_EVENT_RULES: Record<string, EngineEventRule> = {
|
|
167
|
+
ProcessInstanceCreated: {
|
|
168
|
+
kind: "start",
|
|
169
|
+
text: (ev) => `Process ${str(ev, "process_id") ?? "instance"} started`,
|
|
170
|
+
},
|
|
171
|
+
JobCreated: {
|
|
172
|
+
kind: "tool",
|
|
173
|
+
text: (ev) =>
|
|
174
|
+
`Job ${str(ev, "job_type") ?? ""} created`.trim() +
|
|
175
|
+
(elementOf(ev) ? ` on ${elementOf(ev)}` : ""),
|
|
176
|
+
},
|
|
177
|
+
JobCompleted: {
|
|
178
|
+
kind: "vars",
|
|
179
|
+
text: (ev) => `Job ${str(ev, "job_type") ?? ""} completed`.trim(),
|
|
180
|
+
},
|
|
181
|
+
JobFailed: {
|
|
182
|
+
kind: "error",
|
|
183
|
+
text: (ev) => `Job ${str(ev, "job_type") ?? ""} failed`.trim(),
|
|
184
|
+
},
|
|
185
|
+
JobErrorThrown: {
|
|
186
|
+
kind: "error",
|
|
187
|
+
text: (ev) =>
|
|
188
|
+
`Job threw error ${str(ev, "error_code") ?? ""}`.trim() +
|
|
189
|
+
(elementOf(ev) ? ` on ${elementOf(ev)}` : ""),
|
|
190
|
+
},
|
|
191
|
+
IncidentRaised: {
|
|
192
|
+
kind: "error",
|
|
193
|
+
text: (ev) =>
|
|
194
|
+
`Incident on ${elementOf(ev) ?? "instance"}` +
|
|
195
|
+
(str(ev, "reason") ? `: ${str(ev, "reason")}` : ""),
|
|
196
|
+
},
|
|
197
|
+
IncidentResolved: {
|
|
198
|
+
kind: "tool",
|
|
199
|
+
text: (ev) => `Incident resolved on ${elementOf(ev) ?? "instance"}`,
|
|
200
|
+
},
|
|
201
|
+
UserTaskCreated: {
|
|
202
|
+
kind: "human",
|
|
203
|
+
text: (ev) => `User task ${elementOf(ev) ?? ""} awaiting a human`.trim(),
|
|
204
|
+
},
|
|
205
|
+
UserTaskAssigned: {
|
|
206
|
+
kind: "human",
|
|
207
|
+
text: (ev) =>
|
|
208
|
+
`User task ${elementOf(ev) ?? ""} assigned`.trim() +
|
|
209
|
+
(str(ev, "assignee") ? ` to ${str(ev, "assignee")}` : ""),
|
|
210
|
+
},
|
|
211
|
+
UserTaskCompleted: {
|
|
212
|
+
kind: "human",
|
|
213
|
+
text: (ev) => `User task ${elementOf(ev) ?? ""} completed`.trim(),
|
|
214
|
+
},
|
|
215
|
+
UserTaskCanceled: {
|
|
216
|
+
kind: "human",
|
|
217
|
+
text: (ev) => `User task ${elementOf(ev) ?? ""} canceled`.trim(),
|
|
218
|
+
},
|
|
219
|
+
TimerCreated: {
|
|
220
|
+
kind: "tool",
|
|
221
|
+
text: (ev) => `Timer set on ${elementOf(ev) ?? "instance"}`,
|
|
222
|
+
},
|
|
223
|
+
TimerTriggered: {
|
|
224
|
+
kind: "tool",
|
|
225
|
+
text: (ev) => `Timer fired on ${elementOf(ev) ?? "instance"}`,
|
|
226
|
+
},
|
|
227
|
+
MessagePublished: {
|
|
228
|
+
kind: "tool",
|
|
229
|
+
text: (ev) => `Message ${str(ev, "message_name") ?? ""} published`.trim(),
|
|
230
|
+
},
|
|
231
|
+
MessageCorrelated: {
|
|
232
|
+
kind: "tool",
|
|
233
|
+
text: (ev) => `Message ${str(ev, "message_name") ?? ""} correlated`.trim(),
|
|
234
|
+
},
|
|
235
|
+
SignalBroadcast: {
|
|
236
|
+
kind: "tool",
|
|
237
|
+
text: (ev) => `Signal ${str(ev, "signal_name") ?? ""} broadcast`.trim(),
|
|
238
|
+
},
|
|
239
|
+
SignalCorrelated: {
|
|
240
|
+
kind: "tool",
|
|
241
|
+
text: (ev) => `Signal ${str(ev, "signal_name") ?? ""} correlated`.trim(),
|
|
242
|
+
},
|
|
243
|
+
AdHocActivated: {
|
|
244
|
+
kind: "agent",
|
|
245
|
+
text: (ev) => `Ad-hoc sub-process ${elementOf(ev) ?? ""} activated`.trim(),
|
|
246
|
+
},
|
|
247
|
+
AdHocToolActivated: {
|
|
248
|
+
kind: "agent",
|
|
249
|
+
text: (ev) => `Tool ${elementOf(ev) ?? ""} activated`.trim(),
|
|
250
|
+
},
|
|
251
|
+
AdHocToolCompleted: {
|
|
252
|
+
kind: "vars",
|
|
253
|
+
text: (ev) => `Tool ${elementOf(ev) ?? ""} returned`.trim(),
|
|
254
|
+
},
|
|
255
|
+
AdHocCompleted: {
|
|
256
|
+
kind: "agent",
|
|
257
|
+
text: (ev) => `Ad-hoc sub-process ${elementOf(ev) ?? ""} completed`.trim(),
|
|
258
|
+
},
|
|
259
|
+
ProcessInstanceCompleted: {
|
|
260
|
+
kind: "done",
|
|
261
|
+
text: () => "Process completed",
|
|
262
|
+
},
|
|
263
|
+
ProcessInstanceTerminated: {
|
|
264
|
+
kind: "error",
|
|
265
|
+
text: () => "Process terminated",
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The engine-event fold adapter: map a `WasmEvent[]` (the flattened
|
|
271
|
+
* `{ seq, now, type, …snake_case }` stream from `useBojtos().events`) into
|
|
272
|
+
* normalized rows, keeping only the meaningful milestones (see
|
|
273
|
+
* {@link ENGINE_EVENT_RULES}). Each row's `id` is the event's `seq`, so ids stay
|
|
274
|
+
* stable and monotonic across re-reads of a growing log. Engine events carry no
|
|
275
|
+
* turn, so {@link buildTraceItems} over the result is a flat list of rows — the
|
|
276
|
+
* non-agentic test-view shape.
|
|
277
|
+
*/
|
|
278
|
+
export function foldEngineEvents(events: readonly WasmEvent[]): TraceRow[] {
|
|
279
|
+
const rows: TraceRow[] = [];
|
|
280
|
+
for (const ev of events) {
|
|
281
|
+
const rule = ENGINE_EVENT_RULES[ev.type];
|
|
282
|
+
if (!rule) continue;
|
|
283
|
+
rows.push({
|
|
284
|
+
id: ev.seq,
|
|
285
|
+
kind: rule.kind,
|
|
286
|
+
text: rule.text(ev),
|
|
287
|
+
elementId: elementOf(ev),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return rows;
|
|
291
|
+
}
|