@nanobpm/urban 0.82.1 → 0.84.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.
@@ -48,6 +48,278 @@ export function normalizeProcessInstanceState(raw) {
48
48
  return undefined;
49
49
  }
50
50
  }
51
+ /** Map the engine's element-instance `state` onto the transport-agnostic
52
+ * {@link ElementInstanceState} union, or `undefined` for an unrecognized value (which the
53
+ * caller skips rather than surfacing a malformed row). Case-insensitive so a future casing
54
+ * tweak on the engine side doesn't silently drop element instances. Kept separate from
55
+ * {@link normalizeProcessInstanceState} — the two model distinct engine enums that only
56
+ * coincide today — so element and process state can diverge without a hidden coupling. */
57
+ export function normalizeElementInstanceState(raw) {
58
+ if (typeof raw !== "string")
59
+ return undefined;
60
+ switch (raw.toUpperCase()) {
61
+ case "ACTIVE":
62
+ return "ACTIVE";
63
+ case "COMPLETED":
64
+ return "COMPLETED";
65
+ case "TERMINATED":
66
+ return "TERMINATED";
67
+ default:
68
+ return undefined;
69
+ }
70
+ }
71
+ /** Map the engine's wait-state `waitStateType` onto the transport-agnostic
72
+ * {@link WaitStateType} union, or `undefined` for an unrecognized value. Case-insensitive
73
+ * so a casing tweak doesn't silently drop parks. */
74
+ export function normalizeWaitStateType(raw) {
75
+ if (typeof raw !== "string")
76
+ return undefined;
77
+ switch (raw.toUpperCase()) {
78
+ case "JOB":
79
+ return "JOB";
80
+ case "MESSAGE":
81
+ return "MESSAGE";
82
+ case "USER_TASK":
83
+ return "USER_TASK";
84
+ case "TIMER":
85
+ return "TIMER";
86
+ case "SIGNAL":
87
+ return "SIGNAL";
88
+ case "CONDITION":
89
+ return "CONDITION";
90
+ default:
91
+ return undefined;
92
+ }
93
+ }
94
+ /** Map the engine's incident `state` onto the transport-agnostic {@link IncidentState} union,
95
+ * falling back to `"UNKNOWN"` for an unrecognized/absent value (an incident is never dropped
96
+ * for an odd state — unlike element/process rows, whose state is a hard identity field — so a
97
+ * caller still sees the fault). Case-insensitive so a casing tweak doesn't mis-map. */
98
+ export function normalizeIncidentState(raw) {
99
+ if (typeof raw !== "string")
100
+ return "UNKNOWN";
101
+ switch (raw.toUpperCase()) {
102
+ case "ACTIVE":
103
+ return "ACTIVE";
104
+ case "MIGRATED":
105
+ return "MIGRATED";
106
+ case "PENDING":
107
+ return "PENDING";
108
+ case "RESOLVED":
109
+ return "RESOLVED";
110
+ default:
111
+ return "UNKNOWN";
112
+ }
113
+ }
114
+ /** A non-empty string form of an engine key/id, or `undefined` when absent/blank (including a
115
+ * whitespace-only string). Coerces a numeric key to a string (the engine may serialize a key
116
+ * either way) but never `String(...)`-coerces an arbitrary object into a garbage
117
+ * `"[object Object]"` id. The blank check trims, matching `getElementInstance`'s blank-key
118
+ * guard and the form-key presence helpers, so a `" "` key can never leak into a result. */
119
+ function presentEngineKey(value) {
120
+ if (typeof value === "number") {
121
+ return Number.isFinite(value) ? String(value) : undefined;
122
+ }
123
+ if (typeof value !== "string")
124
+ return undefined;
125
+ const trimmed = value.trim();
126
+ return trimmed === "" ? undefined : trimmed;
127
+ }
128
+ /** The non-empty, trimmed string form of a *required* engine key, or throws when the value is
129
+ * absent/blank (including a whitespace-only string). A mutating seam operation addresses a
130
+ * single entity by key, so a blank key is a caller bug that must fail fast with an actionable
131
+ * message rather than reach the engine as an empty path segment / invalid changeset and surface
132
+ * an opaque transport error. Reuses {@link presentEngineKey}'s trim-and-coerce rule so a padded
133
+ * key like `" 5 "` is normalized identically to how read paths resolve it. */
134
+ function requireEngineKey(value, name) {
135
+ const key = presentEngineKey(value);
136
+ if (key === undefined) {
137
+ throw new Error(`${name} must be a non-empty key`);
138
+ }
139
+ return key;
140
+ }
141
+ /** A required text field (a wait-state discriminator's `jobType`/`messageName`/`signalName`),
142
+ * or `undefined` when absent or blank — the caller skips the row rather than emitting a typed
143
+ * wait state carrying an empty required string. Returns the trimmed value, matching the
144
+ * presence rule used for keys, so surrounding whitespace can never leak into a result. */
145
+ function presentText(value) {
146
+ if (typeof value !== "string")
147
+ return undefined;
148
+ const trimmed = value.trim();
149
+ return trimmed === "" ? undefined : trimmed;
150
+ }
151
+ /** The element type carried on an element-instance/wait-state row. The Camunda SDK DTO calls
152
+ * it `type`; a plain REST/Rust-engine body may call it `elementType`. Accept either. */
153
+ function pickElementType(row) {
154
+ return presentText(row.elementType) ?? presentText(row.type);
155
+ }
156
+ /** Map one engine element-instance row onto an {@link ElementInstanceSummary}, or `undefined`
157
+ * when the row is malformed (missing key/elementId/process instance, or an unrecognized
158
+ * state). The single source of truth both `searchElementInstances` and `getElementInstance`
159
+ * extract through, so the two cannot drift. */
160
+ export function mapElementInstanceRow(row, log) {
161
+ const elementInstanceKey = presentEngineKey(row.elementInstanceKey);
162
+ if (elementInstanceKey === undefined) {
163
+ log("warn", "skipping element instance with no key in engine response");
164
+ return undefined;
165
+ }
166
+ const processInstanceKey = presentEngineKey(row.processInstanceKey);
167
+ if (processInstanceKey === undefined) {
168
+ log("warn", "skipping element instance with no processInstanceKey in engine response", {
169
+ elementInstanceKey,
170
+ });
171
+ return undefined;
172
+ }
173
+ const elementId = typeof row.elementId === "string" && row.elementId.trim() !== ""
174
+ ? row.elementId.trim()
175
+ : undefined;
176
+ if (elementId === undefined) {
177
+ log("warn", "skipping element instance with no elementId in engine response", {
178
+ elementInstanceKey,
179
+ });
180
+ return undefined;
181
+ }
182
+ const state = normalizeElementInstanceState(row.state);
183
+ if (state === undefined) {
184
+ log("warn", "skipping element instance with unrecognized state", {
185
+ elementInstanceKey,
186
+ state: String(row.state),
187
+ });
188
+ return undefined;
189
+ }
190
+ const elementType = pickElementType(row);
191
+ return {
192
+ elementInstanceKey,
193
+ processInstanceKey,
194
+ elementId,
195
+ ...(elementType ? { elementType } : {}),
196
+ state,
197
+ };
198
+ }
199
+ /** Map one engine wait-state row onto an {@link ElementInstanceWaitState}, or `undefined`
200
+ * when the row is malformed (missing identity, or an unrecognized `waitStateType`). The
201
+ * Camunda SDK nests the park-specific fields under `details`; a plain REST body may inline
202
+ * them — read `details` first, then fall back to the row itself, so both wire shapes map. */
203
+ export function mapElementInstanceWaitStateRow(row, log) {
204
+ const elementInstanceKey = presentEngineKey(row.elementInstanceKey);
205
+ const processInstanceKey = presentEngineKey(row.processInstanceKey);
206
+ const elementId = typeof row.elementId === "string" && row.elementId.trim() !== ""
207
+ ? row.elementId.trim()
208
+ : undefined;
209
+ if (elementInstanceKey === undefined || processInstanceKey === undefined || elementId === undefined) {
210
+ log("warn", "skipping element-instance wait state with incomplete identity in engine response");
211
+ return undefined;
212
+ }
213
+ const details = isRecord(row.details) ? row.details : row;
214
+ const waitStateType = normalizeWaitStateType(details.waitStateType ?? row.waitStateType);
215
+ if (waitStateType === undefined) {
216
+ log("warn", "skipping element-instance wait state with unrecognized waitStateType", {
217
+ elementInstanceKey,
218
+ waitStateType: String(details.waitStateType ?? row.waitStateType),
219
+ });
220
+ return undefined;
221
+ }
222
+ const elementType = pickElementType(row);
223
+ const base = {
224
+ elementInstanceKey,
225
+ processInstanceKey,
226
+ elementId,
227
+ ...(elementType ? { elementType } : {}),
228
+ };
229
+ switch (waitStateType) {
230
+ case "JOB": {
231
+ // `jobType` is the required discriminator of a JOB park; without it the row is malformed.
232
+ const jobType = presentText(details.jobType);
233
+ if (jobType === undefined) {
234
+ log("warn", "skipping JOB wait state with no jobType in engine response", {
235
+ elementInstanceKey,
236
+ });
237
+ return undefined;
238
+ }
239
+ const jobKey = presentEngineKey(details.jobKey);
240
+ return { ...base, waitStateType, jobType, ...(jobKey ? { jobKey } : {}) };
241
+ }
242
+ case "MESSAGE": {
243
+ // `messageName` is the required discriminator of a MESSAGE park.
244
+ const messageName = presentText(details.messageName);
245
+ if (messageName === undefined) {
246
+ log("warn", "skipping MESSAGE wait state with no messageName in engine response", {
247
+ elementInstanceKey,
248
+ });
249
+ return undefined;
250
+ }
251
+ const correlationKey = presentText(details.correlationKey);
252
+ return {
253
+ ...base,
254
+ waitStateType,
255
+ messageName,
256
+ ...(correlationKey ? { correlationKey } : {}),
257
+ };
258
+ }
259
+ case "USER_TASK": {
260
+ // `userTaskKey` is the required identity of a USER_TASK park; without it the row is
261
+ // malformed. Skip it (like the missing-identity guards above) rather than emit a typed
262
+ // wait state carrying an empty key that would break a downstream join.
263
+ const userTaskKey = presentEngineKey(details.taskKey ?? details.userTaskKey);
264
+ if (userTaskKey === undefined) {
265
+ log("warn", "skipping USER_TASK wait state with no userTaskKey in engine response", {
266
+ elementInstanceKey,
267
+ });
268
+ return undefined;
269
+ }
270
+ return { ...base, waitStateType, userTaskKey };
271
+ }
272
+ case "SIGNAL": {
273
+ // `signalName` is the required discriminator of a SIGNAL park.
274
+ const signalName = presentText(details.signalName);
275
+ if (signalName === undefined) {
276
+ log("warn", "skipping SIGNAL wait state with no signalName in engine response", {
277
+ elementInstanceKey,
278
+ });
279
+ return undefined;
280
+ }
281
+ return { ...base, waitStateType, signalName };
282
+ }
283
+ case "TIMER":
284
+ return { ...base, waitStateType };
285
+ case "CONDITION":
286
+ return { ...base, waitStateType };
287
+ }
288
+ }
289
+ /** Map one engine incident row onto an {@link IncidentSummary}, or `undefined` when the row is
290
+ * malformed (missing the `incidentKey` or the owning `processInstanceKey` — the two identity
291
+ * fields a caller needs to resolve/act on it). `jobKey`/`elementInstanceKey`/`errorType`/
292
+ * `errorMessage` are best-effort diagnostics: present when the engine reports them, omitted
293
+ * otherwise. The single source of truth `searchIncidents` extracts through. */
294
+ export function mapIncidentRow(row, log) {
295
+ const incidentKey = presentEngineKey(row.incidentKey);
296
+ if (incidentKey === undefined) {
297
+ log("warn", "skipping incident with no incidentKey in engine response");
298
+ return undefined;
299
+ }
300
+ const processInstanceKey = presentEngineKey(row.processInstanceKey);
301
+ if (processInstanceKey === undefined) {
302
+ log("warn", "skipping incident with no processInstanceKey in engine response", {
303
+ incidentKey,
304
+ });
305
+ return undefined;
306
+ }
307
+ const elementId = presentText(row.elementId);
308
+ const elementInstanceKey = presentEngineKey(row.elementInstanceKey);
309
+ const jobKey = presentEngineKey(row.jobKey);
310
+ const errorType = presentText(row.errorType);
311
+ const errorMessage = presentText(row.errorMessage);
312
+ return {
313
+ incidentKey,
314
+ processInstanceKey,
315
+ ...(elementId ? { elementId } : {}),
316
+ ...(elementInstanceKey ? { elementInstanceKey } : {}),
317
+ ...(jobKey ? { jobKey } : {}),
318
+ ...(errorType ? { errorType } : {}),
319
+ ...(errorMessage ? { errorMessage } : {}),
320
+ state: normalizeIncidentState(row.state),
321
+ };
322
+ }
51
323
  /**
52
324
  * An `EngineClient` backed entirely by a `@nanobpm/nano-sdk` client. Deploy, create,
53
325
  * message, and user-task calls map one-to-one onto the SDK's client methods; workers
@@ -215,6 +487,121 @@ export class SdkEngineClient {
215
487
  return [{ processInstanceKey: String(key), state }];
216
488
  });
217
489
  }
490
+ async searchElementInstances(filter) {
491
+ // Mirror `searchProcessInstances`: build the engine filter from the transport-agnostic
492
+ // selectors, read at zero-wait consistency (an element-instance search is eventually
493
+ // consistent), and map each row through the single `mapElementInstanceRow` gate — which
494
+ // also `searchElementInstances`/`getElementInstance` share — dropping malformed rows.
495
+ const f = {};
496
+ if (filter?.processInstanceKey)
497
+ f.processInstanceKey = filter.processInstanceKey;
498
+ if (filter?.elementId)
499
+ f.elementId = filter.elementId;
500
+ if (filter?.state)
501
+ f.state = filter.state;
502
+ const body = await this.client.searchElementInstances({ filter: f }, { consistency: { waitUpToMs: 0 } });
503
+ const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
504
+ return items.flatMap((it) => {
505
+ const mapped = mapElementInstanceRow(it, this.log);
506
+ return mapped ? [mapped] : [];
507
+ });
508
+ }
509
+ async searchElementInstanceWaitStates(filter) {
510
+ // The wait-states search surfaces *every* park (job/message/timer/signal/condition/user
511
+ // task), not just user tasks. Same zero-wait read + per-row mapping-gate shape as
512
+ // `searchElementInstances`; the SDK nests park-specific fields under `details`, which
513
+ // `mapElementInstanceWaitStateRow` unwraps.
514
+ const f = {};
515
+ if (filter?.processInstanceKey)
516
+ f.processInstanceKey = filter.processInstanceKey;
517
+ if (filter?.elementId)
518
+ f.elementId = filter.elementId;
519
+ if (filter?.waitStateType)
520
+ f.waitStateType = filter.waitStateType;
521
+ const body = await this.client.searchElementInstanceWaitStates({ filter: f }, { consistency: { waitUpToMs: 0 } });
522
+ const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
523
+ return items.flatMap((it) => {
524
+ const mapped = mapElementInstanceWaitStateRow(it, this.log);
525
+ return mapped ? [mapped] : [];
526
+ });
527
+ }
528
+ async getElementInstance(elementInstanceKey) {
529
+ // A blank key can never address an element instance — short-circuit to null rather than
530
+ // issue a `GET /element-instances/` with an empty segment. Normalize the key up front (as
531
+ // `getForm` resolves a normalized identifier before fetching) so a padded-but-valid key
532
+ // like `" 5 "` addresses the same element instance rather than 404ing on the raw segment.
533
+ if (typeof elementInstanceKey !== "string" || elementInstanceKey.trim() === "") {
534
+ return null;
535
+ }
536
+ const key = elementInstanceKey.trim();
537
+ let body;
538
+ try {
539
+ body = await this.client.getElementInstance({ elementInstanceKey: key }, { consistency: { waitUpToMs: 0 } });
540
+ }
541
+ catch (err) {
542
+ // A 404 (no such element instance) is an expected "not found", not a fault — mirror
543
+ // `getForm`, which treats a failed fetch as absence (null) rather than propagating.
544
+ this.log("warn", "getElementInstance: engine fetch failed", {
545
+ elementInstanceKey: key,
546
+ err: err instanceof Error ? err.message : String(err),
547
+ });
548
+ return null;
549
+ }
550
+ if (!isRecord(body))
551
+ return null;
552
+ return mapElementInstanceRow(body, this.log) ?? null;
553
+ }
554
+ async searchIncidents(filter) {
555
+ // Same shape as `searchElementInstances`: build the engine filter from the transport-agnostic
556
+ // selectors, read at zero-wait consistency (an incident search is eventually consistent), and
557
+ // map each row through the single `mapIncidentRow` gate, dropping malformed rows.
558
+ const f = {};
559
+ // Normalize the request selectors the same way the response mapper normalizes keys: a
560
+ // whitespace-only/padded `processInstanceKey` must not reach the engine as a garbage filter
561
+ // segment (which would silently return surprising empty results).
562
+ const processInstanceKey = presentEngineKey(filter?.processInstanceKey);
563
+ if (processInstanceKey)
564
+ f.processInstanceKey = processInstanceKey;
565
+ if (filter?.state)
566
+ f.state = filter.state;
567
+ const body = await this.client.searchIncidents({ filter: f }, { consistency: { waitUpToMs: 0 } });
568
+ const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
569
+ return items.flatMap((it) => {
570
+ const mapped = mapIncidentRow(it, this.log);
571
+ return mapped ? [mapped] : [];
572
+ });
573
+ }
574
+ async resolveIncident(input) {
575
+ await this.client.resolveIncident({
576
+ incidentKey: requireEngineKey(input.incidentKey, "incidentKey"),
577
+ });
578
+ }
579
+ async updateJobRetries(input) {
580
+ // The "retry a failed job" operation: a job's remaining retries are the `retries` field of
581
+ // the `updateJob` changeset. Bumping them back above zero makes a failed job activatable so a
582
+ // paired `resolveIncident` can return it to the pool.
583
+ const jobKey = requireEngineKey(input.jobKey, "jobKey");
584
+ // `retries` serializes straight into the engine changeset, and `number` admits `NaN`,
585
+ // fractional, and negative values — none of which is a valid retry count. Reject them up
586
+ // front so a tool-driven call fails with a clear message instead of an opaque engine error.
587
+ if (!Number.isInteger(input.retries) || input.retries < 0) {
588
+ throw new Error("retries must be a non-negative integer");
589
+ }
590
+ await this.client.updateJob({
591
+ jobKey,
592
+ changeset: { retries: input.retries },
593
+ });
594
+ }
595
+ async setVariables(input) {
596
+ // A scope is addressed by its element-instance key (the process instance's root scope is an
597
+ // element instance too), so `scopeKey` maps straight onto `elementInstanceKey`. `local`
598
+ // defaults to engine behaviour (propagate to the outermost scope) when unset.
599
+ await this.client.createElementInstanceVariables({
600
+ elementInstanceKey: requireEngineKey(input.scopeKey, "scopeKey"),
601
+ variables: input.variables,
602
+ ...(input.local === undefined ? {} : { local: input.local }),
603
+ });
604
+ }
218
605
  async registerWorker(jobType, handler, options) {
219
606
  const worker = this.client.createJobWorker({
220
607
  jobType,
@@ -8,7 +8,7 @@ export { bindModeToHost, expandEnv, expandEnvString, isBindMode, loadManifest, p
8
8
  export type { ActionDecl, AppManifest, BindMode, ChatSurface, DataSource, DomainField, DomainType, InstanceTracking, LlmBinding, NetworkConfig, PagesSurface, Surfaces, TaskInboxSurface, Trigger, Worker, } from "./core/manifest.ts";
9
9
  export { collectManifestIssues, ManifestValidationError, validateManifest, } from "./core/validate.ts";
10
10
  export type { ValidationIssue } from "./core/validate.ts";
11
- export type { EngineClient, EngineJob, FormSchema, HostContext, HttpHandler, HttpRequest, HttpResponse, HttpServer, JobHandler, ProcessInstanceSnapshot, ProcessInstanceState, SqliteDb, UserTaskState, UserTaskSummary, UserTaskFilter, WatchHandle, WorkerSubscription, } from "./core/host.ts";
11
+ export type { EngineClient, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, EngineJob, FormSchema, HostContext, HttpHandler, HttpRequest, HttpResponse, HttpServer, IncidentState, EngineIncidentState, IncidentFilter, IncidentSummary, JobHandler, ProcessInstanceSnapshot, ProcessInstanceState, SqliteDb, UserTaskState, UserTaskSummary, UserTaskFilter, WatchHandle, WorkerSubscription, } from "./core/host.ts";
12
12
  export { buildFormSchema, parseFormSchema, pickFormLinkage, presentFormIdentifier, resolveFormIdentifier, } from "./core/form-contract.ts";
13
13
  export type { AppApi, Mounted, RuntimeContext } from "./core/context.ts";
14
14
  export { createLogger } from "./core/logger.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.82.1",
3
+ "version": "0.84.0",
4
4
  "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -261,6 +261,153 @@ export interface UserTaskFilter {
261
261
  candidateGroup?: string;
262
262
  }
263
263
 
264
+ /**
265
+ * An element instance's lifecycle state, as the engine reports it on a
266
+ * `/v2/element-instances/search` result. Mirrors the engine's `ElementInstanceStateEnum`:
267
+ * `ACTIVE` (a token is currently *at* this element), `COMPLETED` (the token has left it), or
268
+ * `TERMINATED` (the element was interrupted). Structurally the same state set as
269
+ * {@link ProcessInstanceState} (one active state plus the `COMPLETED`/`TERMINATED` terminals),
270
+ * but a distinct concept — element instances are the tokens *within* a process instance — so it
271
+ * is kept as its own type rather than aliased.
272
+ */
273
+ export type ElementInstanceState = "ACTIVE" | "COMPLETED" | "TERMINATED";
274
+
275
+ /**
276
+ * A single element instance ("token") as {@link EngineClient.searchElementInstances} and
277
+ * {@link EngineClient.getElementInstance} report it — the finest-grained "how far has this
278
+ * process instance progressed" signal, below the process-instance lifecycle
279
+ * ({@link ProcessInstanceSnapshot}) and independent of whether the element is a user task
280
+ * ({@link UserTaskSummary}). It surfaces active *non-user-task* elements (a service task, a
281
+ * gateway, a catch event) a user-task search cannot see — "the furthest element reached".
282
+ * Carries the element instance's own key, its identity (`elementId`, its BPMN `elementType`
283
+ * when the engine reports it), the owning process instance, and its lifecycle `state`.
284
+ */
285
+ export interface ElementInstanceSummary {
286
+ readonly elementInstanceKey: string;
287
+ readonly processInstanceKey: string;
288
+ readonly elementId: string;
289
+ /** The BPMN element type (e.g. `"SERVICE_TASK"`, `"USER_TASK"`,
290
+ * `"INTERMEDIATE_CATCH_EVENT"`), when the engine reports it. Kept transport-agnostic (a
291
+ * bare string) so a new BPMN element type on the engine flows through without having to be
292
+ * enumerated here. Absent when the adapter's read model does not carry it. */
293
+ readonly elementType?: string;
294
+ readonly state: ElementInstanceState;
295
+ }
296
+
297
+ /**
298
+ * The kind of wait an element instance is parked in, as a
299
+ * `/v2/element-instances/wait-states/search` result reports it. Unlike a user-task search,
300
+ * this surfaces *every* park — a `JOB` (a service task awaiting a worker), a `MESSAGE`
301
+ * (an event awaiting correlation), a `TIMER`, a `SIGNAL`, a `CONDITION`, as well as a
302
+ * `USER_TASK` — so a consumer can read "what is this instance blocked on" beyond user tasks.
303
+ */
304
+ export type WaitStateType =
305
+ | "JOB"
306
+ | "MESSAGE"
307
+ | "USER_TASK"
308
+ | "TIMER"
309
+ | "SIGNAL"
310
+ | "CONDITION";
311
+
312
+ /**
313
+ * A single parked (waiting) element instance as
314
+ * {@link EngineClient.searchElementInstanceWaitStates} reports it. Every variant carries the
315
+ * element instance's identity; the `waitStateType` discriminates the park-specific fields — a
316
+ * `JOB` carries the `jobType`/`jobKey` it is parked on, a `MESSAGE` the awaited
317
+ * `messageName`/`correlationKey`, a `USER_TASK` the `userTaskKey`, a `SIGNAL` the
318
+ * `signalName` — so a caller narrows on `waitStateType` to reach them type-safely rather than
319
+ * probing a flat optional-field bag.
320
+ */
321
+ export type ElementInstanceWaitState =
322
+ & {
323
+ readonly elementInstanceKey: string;
324
+ readonly processInstanceKey: string;
325
+ readonly elementId: string;
326
+ readonly elementType?: string;
327
+ }
328
+ & (
329
+ | { readonly waitStateType: "JOB"; readonly jobType: string; readonly jobKey?: string }
330
+ | {
331
+ readonly waitStateType: "MESSAGE";
332
+ readonly messageName: string;
333
+ readonly correlationKey?: string;
334
+ }
335
+ | { readonly waitStateType: "USER_TASK"; readonly userTaskKey: string }
336
+ | { readonly waitStateType: "TIMER" }
337
+ | { readonly waitStateType: "SIGNAL"; readonly signalName: string }
338
+ | { readonly waitStateType: "CONDITION" }
339
+ );
340
+
341
+ /**
342
+ * Selectors for an element-instance search: which process instance, element, and/or lifecycle
343
+ * state to match. Used by {@link EngineClient.searchElementInstances}.
344
+ */
345
+ export interface ElementInstanceFilter {
346
+ processInstanceKey?: string;
347
+ elementId?: string;
348
+ state?: ElementInstanceState;
349
+ }
350
+
351
+ /**
352
+ * Selectors for an element-instance *wait-state* search: which process instance, element,
353
+ * and/or kind of wait to match. Used by {@link EngineClient.searchElementInstanceWaitStates}.
354
+ * There is deliberately no lifecycle-`state` selector — a wait state is always an active park.
355
+ */
356
+ export interface ElementInstanceWaitStateFilter {
357
+ processInstanceKey?: string;
358
+ elementId?: string;
359
+ waitStateType?: WaitStateType;
360
+ }
361
+
362
+ /**
363
+ * The engine's own incident-state enum — the lifecycle states an engine actually reports and
364
+ * that are valid as a search selector. An `ACTIVE` incident is an open fault a human/agent can
365
+ * act on (resolve/retry); the other values are terminal or transitional. This is the single
366
+ * source of truth for real engine states; {@link IncidentState} derives from it.
367
+ */
368
+ export type EngineIncidentState = "ACTIVE" | "MIGRATED" | "PENDING" | "RESOLVED";
369
+
370
+ /**
371
+ * The lifecycle state of an incident, as {@link EngineClient.searchIncidents} reports it.
372
+ * Extends {@link EngineIncidentState} with the client-side `"UNKNOWN"` sentinel — a fallback
373
+ * used when the engine returns an unrecognized/absent value, so an incident is surfaced rather
374
+ * than dropped for an odd state. Because `"UNKNOWN"` is not a real engine state, it is not a
375
+ * valid {@link IncidentFilter} selector. A caller narrows on this union rather than probing a
376
+ * raw string.
377
+ */
378
+ export type IncidentState = EngineIncidentState | "UNKNOWN";
379
+
380
+ /**
381
+ * Selectors for an incident search: which process instance and/or lifecycle state to match.
382
+ * Used by {@link EngineClient.searchIncidents}. An eventually consistent (zero-wait) read; an
383
+ * unset selector matches every incident.
384
+ */
385
+ export interface IncidentFilter {
386
+ processInstanceKey?: string;
387
+ state?: EngineIncidentState;
388
+ }
389
+
390
+ /**
391
+ * A single incident (a stuck token — a job out of retries, an unhandled error, a failed
392
+ * gateway evaluation) as {@link EngineClient.searchIncidents} reports it. Carries the
393
+ * `incidentKey` a caller passes to {@link EngineClient.resolveIncident}, the owning
394
+ * `processInstanceKey`, and — when the incident is job-backed — the `jobKey` a caller passes
395
+ * to {@link EngineClient.updateJobRetries} to make the job retriable before resolving.
396
+ * `errorType`/`errorMessage` are the engine's fault description; they are free-form across
397
+ * engines (the live SDK reports a Camunda error-type enum, the in-process engine a coarser
398
+ * `kind`), so treat them as diagnostic text, not a stable taxonomy.
399
+ */
400
+ export interface IncidentSummary {
401
+ readonly incidentKey: string;
402
+ readonly processInstanceKey: string;
403
+ readonly elementId?: string;
404
+ readonly elementInstanceKey?: string;
405
+ readonly jobKey?: string;
406
+ readonly errorType?: string;
407
+ readonly errorMessage?: string;
408
+ readonly state: IncidentState;
409
+ }
410
+
264
411
  /**
265
412
  * A deployed form's form-js schema plus its identifying metadata, as
266
413
  * {@link EngineClient.getForm} returns it. `schema` is the parsed form-js document
@@ -350,6 +497,77 @@ export interface EngineClient {
350
497
  processInstanceKeys?: string[];
351
498
  state?: ProcessInstanceState;
352
499
  }): Promise<ProcessInstanceSnapshot[]>;
500
+ /**
501
+ * Search element instances ("tokens") by process instance, element, and/or lifecycle state
502
+ * (`POST /v2/element-instances/search`). Where {@link searchProcessInstances} answers "is
503
+ * this instance still running" and {@link searchUserTasks} answers "what user tasks are
504
+ * open", this answers "what element has a token reached" — including the active
505
+ * *non-user-task* elements (a service task, a gateway, a catch event) that a user-task
506
+ * search cannot see, i.e. "the furthest element reached". An eventually consistent
507
+ * (zero-wait) read; each {@link ElementInstanceSummary} carries the element instance's key,
508
+ * its `elementId`/`elementType`, the owning process instance, and its `state`.
509
+ */
510
+ searchElementInstances(
511
+ filter?: ElementInstanceFilter,
512
+ ): Promise<ElementInstanceSummary[]>;
513
+ /**
514
+ * Search the *wait states* of element instances
515
+ * (`POST /v2/element-instances/wait-states/search`) — every park, not only user tasks. A
516
+ * `JOB` park (a service task awaiting a worker), a `MESSAGE` park (an event awaiting
517
+ * correlation), a `TIMER`/`SIGNAL`/`CONDITION`, plus `USER_TASK`. A zero-wait read; each
518
+ * result is discriminated by `waitStateType` (see {@link ElementInstanceWaitState}), so a
519
+ * consumer can read the job/message parks a {@link searchUserTasks} cannot surface.
520
+ */
521
+ searchElementInstanceWaitStates(
522
+ filter?: ElementInstanceWaitStateFilter,
523
+ ): Promise<ElementInstanceWaitState[]>;
524
+ /**
525
+ * Fetch a single element instance by its key (`GET /v2/element-instances/{elementInstanceKey}`),
526
+ * or `null` when it cannot be resolved — the key is blank, no such element instance exists
527
+ * (a 404), or the fetch otherwise fails. A read is treated as absence rather than propagating
528
+ * (mirroring {@link getForm}), so a caller distinguishes "have it" from "don't" without a
529
+ * try/catch. A zero-wait read returning the same {@link ElementInstanceSummary} shape as
530
+ * {@link searchElementInstances}.
531
+ */
532
+ getElementInstance(
533
+ elementInstanceKey: string,
534
+ ): Promise<ElementInstanceSummary | null>;
535
+ /**
536
+ * Search incidents by owning process instance and/or lifecycle state
537
+ * (`POST /v2/incidents/search`) — the open faults blocking one or more instances (a job out
538
+ * of retries, an unhandled error, a failed gateway evaluation). An eventually consistent
539
+ * (zero-wait) read; each {@link IncidentSummary} carries the `incidentKey` to pass to
540
+ * {@link resolveIncident} and, for a job-backed incident, the `jobKey` to pass to
541
+ * {@link updateJobRetries}. The debugging counterpart to the read accessors above: where
542
+ * they answer "how far has this instance progressed", this answers "why is it stuck".
543
+ */
544
+ searchIncidents(filter?: IncidentFilter): Promise<IncidentSummary[]>;
545
+ /**
546
+ * Resolve an open incident by key (`POST /v2/incidents/{incidentKey}/resolution`), unblocking
547
+ * the parked token: a job incident returns the job to the activatable pool (so it must have
548
+ * retries left first — see {@link updateJobRetries}), a gateway incident re-evaluates, an
549
+ * uncaught-error incident re-creates the service-task job. A mutating operation.
550
+ */
551
+ resolveIncident(input: { incidentKey: string }): Promise<void>;
552
+ /**
553
+ * Update the remaining retries of a job (`PATCH /v2/jobs/{jobKey}`) — the "retry a failed
554
+ * job" operation. Setting a failed job's retries back above zero makes it activatable again;
555
+ * paired with {@link resolveIncident}, this is how a stuck `jobNoRetries` incident is cleared
556
+ * (bump retries, then resolve). A mutating operation.
557
+ */
558
+ updateJobRetries(input: { jobKey: string; retries: number }): Promise<void>;
559
+ /**
560
+ * Set (merge) variables into a scope (`PUT /v2/element-instances/{scopeKey}/variables`).
561
+ * `scopeKey` is a process-instance key or an element-instance key; when `local` is `true` the
562
+ * variables are merged strictly into that local scope, otherwise (the default) they propagate
563
+ * to the outermost scope. A mutating operation used to repair state before resolving an
564
+ * incident or to steer an instance during debugging.
565
+ */
566
+ setVariables(input: {
567
+ scopeKey: string;
568
+ variables: Record<string, unknown>;
569
+ local?: boolean;
570
+ }): Promise<void>;
353
571
  /** Register a push worker for a job type. Draining is handled by the adapter. */
354
572
  registerWorker(
355
573
  jobType: string,
@@ -384,6 +602,13 @@ export const ENGINE_CLIENT_METHODS = [
384
602
  "getForm",
385
603
  "completeUserTask",
386
604
  "searchProcessInstances",
605
+ "searchElementInstances",
606
+ "searchElementInstanceWaitStates",
607
+ "getElementInstance",
608
+ "searchIncidents",
609
+ "resolveIncident",
610
+ "updateJobRetries",
611
+ "setVariables",
387
612
  "registerWorker",
388
613
  "close",
389
614
  ] as const satisfies readonly (keyof EngineClient)[];