@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.
- package/dist/runtime/core/host.d.ts +207 -1
- package/dist/runtime/core/host.js +7 -0
- package/dist/runtime/engine/nanosdk.d.ts +76 -1
- package/dist/runtime/engine/nanosdk.js +387 -0
- package/dist/runtime/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/runtime/core/host.ts +225 -0
- package/src/runtime/core/modules/actions.test.ts +7 -0
- package/src/runtime/core/modules/agent.test.ts +7 -0
- package/src/runtime/core/modules/api.test.ts +7 -0
- package/src/runtime/core/modules/cancel.test.ts +7 -0
- package/src/runtime/core/modules/connectors.test.ts +7 -0
- package/src/runtime/core/modules/datasource.migrations.test.ts +7 -0
- package/src/runtime/core/modules/instance-tracking.test.ts +7 -0
- package/src/runtime/core/modules/pages.test.ts +7 -0
- package/src/runtime/core/modules/surfaces.test.ts +14 -0
- package/src/runtime/core/modules/triggers.test.ts +7 -0
- package/src/runtime/core/modules/workers.test.ts +7 -0
- package/src/runtime/core/runtime.extensions.test.ts +15 -0
- package/src/runtime/core/runtime.test.ts +15 -0
- package/src/runtime/engine/nanosdk.test.ts +384 -0
- package/src/runtime/engine/nanosdk.ts +463 -0
- package/src/runtime/index.ts +10 -0
|
@@ -13,8 +13,17 @@
|
|
|
13
13
|
|
|
14
14
|
import type {
|
|
15
15
|
EngineClient,
|
|
16
|
+
ElementInstanceState,
|
|
17
|
+
ElementInstanceSummary,
|
|
18
|
+
ElementInstanceFilter,
|
|
19
|
+
ElementInstanceWaitState,
|
|
20
|
+
ElementInstanceWaitStateFilter,
|
|
21
|
+
WaitStateType,
|
|
16
22
|
EngineJob,
|
|
17
23
|
FormSchema,
|
|
24
|
+
IncidentFilter,
|
|
25
|
+
IncidentState,
|
|
26
|
+
IncidentSummary,
|
|
18
27
|
JobHandler,
|
|
19
28
|
ProcessInstanceSnapshot,
|
|
20
29
|
ProcessInstanceState,
|
|
@@ -63,6 +72,294 @@ export function normalizeProcessInstanceState(
|
|
|
63
72
|
}
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
/** Map the engine's element-instance `state` onto the transport-agnostic
|
|
76
|
+
* {@link ElementInstanceState} union, or `undefined` for an unrecognized value (which the
|
|
77
|
+
* caller skips rather than surfacing a malformed row). Case-insensitive so a future casing
|
|
78
|
+
* tweak on the engine side doesn't silently drop element instances. Kept separate from
|
|
79
|
+
* {@link normalizeProcessInstanceState} — the two model distinct engine enums that only
|
|
80
|
+
* coincide today — so element and process state can diverge without a hidden coupling. */
|
|
81
|
+
export function normalizeElementInstanceState(
|
|
82
|
+
raw: unknown,
|
|
83
|
+
): ElementInstanceState | undefined {
|
|
84
|
+
if (typeof raw !== "string") return undefined;
|
|
85
|
+
switch (raw.toUpperCase()) {
|
|
86
|
+
case "ACTIVE":
|
|
87
|
+
return "ACTIVE";
|
|
88
|
+
case "COMPLETED":
|
|
89
|
+
return "COMPLETED";
|
|
90
|
+
case "TERMINATED":
|
|
91
|
+
return "TERMINATED";
|
|
92
|
+
default:
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Map the engine's wait-state `waitStateType` onto the transport-agnostic
|
|
98
|
+
* {@link WaitStateType} union, or `undefined` for an unrecognized value. Case-insensitive
|
|
99
|
+
* so a casing tweak doesn't silently drop parks. */
|
|
100
|
+
export function normalizeWaitStateType(raw: unknown): WaitStateType | undefined {
|
|
101
|
+
if (typeof raw !== "string") return undefined;
|
|
102
|
+
switch (raw.toUpperCase()) {
|
|
103
|
+
case "JOB":
|
|
104
|
+
return "JOB";
|
|
105
|
+
case "MESSAGE":
|
|
106
|
+
return "MESSAGE";
|
|
107
|
+
case "USER_TASK":
|
|
108
|
+
return "USER_TASK";
|
|
109
|
+
case "TIMER":
|
|
110
|
+
return "TIMER";
|
|
111
|
+
case "SIGNAL":
|
|
112
|
+
return "SIGNAL";
|
|
113
|
+
case "CONDITION":
|
|
114
|
+
return "CONDITION";
|
|
115
|
+
default:
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Map the engine's incident `state` onto the transport-agnostic {@link IncidentState} union,
|
|
121
|
+
* falling back to `"UNKNOWN"` for an unrecognized/absent value (an incident is never dropped
|
|
122
|
+
* for an odd state — unlike element/process rows, whose state is a hard identity field — so a
|
|
123
|
+
* caller still sees the fault). Case-insensitive so a casing tweak doesn't mis-map. */
|
|
124
|
+
export function normalizeIncidentState(raw: unknown): IncidentState {
|
|
125
|
+
if (typeof raw !== "string") return "UNKNOWN";
|
|
126
|
+
switch (raw.toUpperCase()) {
|
|
127
|
+
case "ACTIVE":
|
|
128
|
+
return "ACTIVE";
|
|
129
|
+
case "MIGRATED":
|
|
130
|
+
return "MIGRATED";
|
|
131
|
+
case "PENDING":
|
|
132
|
+
return "PENDING";
|
|
133
|
+
case "RESOLVED":
|
|
134
|
+
return "RESOLVED";
|
|
135
|
+
default:
|
|
136
|
+
return "UNKNOWN";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A non-empty string form of an engine key/id, or `undefined` when absent/blank (including a
|
|
141
|
+
* whitespace-only string). Coerces a numeric key to a string (the engine may serialize a key
|
|
142
|
+
* either way) but never `String(...)`-coerces an arbitrary object into a garbage
|
|
143
|
+
* `"[object Object]"` id. The blank check trims, matching `getElementInstance`'s blank-key
|
|
144
|
+
* guard and the form-key presence helpers, so a `" "` key can never leak into a result. */
|
|
145
|
+
function presentEngineKey(value: unknown): string | undefined {
|
|
146
|
+
if (typeof value === "number") {
|
|
147
|
+
return Number.isFinite(value) ? String(value) : undefined;
|
|
148
|
+
}
|
|
149
|
+
if (typeof value !== "string") return undefined;
|
|
150
|
+
const trimmed = value.trim();
|
|
151
|
+
return trimmed === "" ? undefined : trimmed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The non-empty, trimmed string form of a *required* engine key, or throws when the value is
|
|
155
|
+
* absent/blank (including a whitespace-only string). A mutating seam operation addresses a
|
|
156
|
+
* single entity by key, so a blank key is a caller bug that must fail fast with an actionable
|
|
157
|
+
* message rather than reach the engine as an empty path segment / invalid changeset and surface
|
|
158
|
+
* an opaque transport error. Reuses {@link presentEngineKey}'s trim-and-coerce rule so a padded
|
|
159
|
+
* key like `" 5 "` is normalized identically to how read paths resolve it. */
|
|
160
|
+
function requireEngineKey(value: unknown, name: string): string {
|
|
161
|
+
const key = presentEngineKey(value);
|
|
162
|
+
if (key === undefined) {
|
|
163
|
+
throw new Error(`${name} must be a non-empty key`);
|
|
164
|
+
}
|
|
165
|
+
return key;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A required text field (a wait-state discriminator's `jobType`/`messageName`/`signalName`),
|
|
169
|
+
* or `undefined` when absent or blank — the caller skips the row rather than emitting a typed
|
|
170
|
+
* wait state carrying an empty required string. Returns the trimmed value, matching the
|
|
171
|
+
* presence rule used for keys, so surrounding whitespace can never leak into a result. */
|
|
172
|
+
function presentText(value: unknown): string | undefined {
|
|
173
|
+
if (typeof value !== "string") return undefined;
|
|
174
|
+
const trimmed = value.trim();
|
|
175
|
+
return trimmed === "" ? undefined : trimmed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The element type carried on an element-instance/wait-state row. The Camunda SDK DTO calls
|
|
179
|
+
* it `type`; a plain REST/Rust-engine body may call it `elementType`. Accept either. */
|
|
180
|
+
function pickElementType(row: Record<string, unknown>): string | undefined {
|
|
181
|
+
return presentText(row.elementType) ?? presentText(row.type);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Map one engine element-instance row onto an {@link ElementInstanceSummary}, or `undefined`
|
|
185
|
+
* when the row is malformed (missing key/elementId/process instance, or an unrecognized
|
|
186
|
+
* state). The single source of truth both `searchElementInstances` and `getElementInstance`
|
|
187
|
+
* extract through, so the two cannot drift. */
|
|
188
|
+
export function mapElementInstanceRow(
|
|
189
|
+
row: Record<string, unknown>,
|
|
190
|
+
log: Log,
|
|
191
|
+
): ElementInstanceSummary | undefined {
|
|
192
|
+
const elementInstanceKey = presentEngineKey(row.elementInstanceKey);
|
|
193
|
+
if (elementInstanceKey === undefined) {
|
|
194
|
+
log("warn", "skipping element instance with no key in engine response");
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
const processInstanceKey = presentEngineKey(row.processInstanceKey);
|
|
198
|
+
if (processInstanceKey === undefined) {
|
|
199
|
+
log("warn", "skipping element instance with no processInstanceKey in engine response", {
|
|
200
|
+
elementInstanceKey,
|
|
201
|
+
});
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
const elementId = typeof row.elementId === "string" && row.elementId.trim() !== ""
|
|
205
|
+
? row.elementId.trim()
|
|
206
|
+
: undefined;
|
|
207
|
+
if (elementId === undefined) {
|
|
208
|
+
log("warn", "skipping element instance with no elementId in engine response", {
|
|
209
|
+
elementInstanceKey,
|
|
210
|
+
});
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
const state = normalizeElementInstanceState(row.state);
|
|
214
|
+
if (state === undefined) {
|
|
215
|
+
log("warn", "skipping element instance with unrecognized state", {
|
|
216
|
+
elementInstanceKey,
|
|
217
|
+
state: String(row.state),
|
|
218
|
+
});
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
const elementType = pickElementType(row);
|
|
222
|
+
return {
|
|
223
|
+
elementInstanceKey,
|
|
224
|
+
processInstanceKey,
|
|
225
|
+
elementId,
|
|
226
|
+
...(elementType ? { elementType } : {}),
|
|
227
|
+
state,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Map one engine wait-state row onto an {@link ElementInstanceWaitState}, or `undefined`
|
|
232
|
+
* when the row is malformed (missing identity, or an unrecognized `waitStateType`). The
|
|
233
|
+
* Camunda SDK nests the park-specific fields under `details`; a plain REST body may inline
|
|
234
|
+
* them — read `details` first, then fall back to the row itself, so both wire shapes map. */
|
|
235
|
+
export function mapElementInstanceWaitStateRow(
|
|
236
|
+
row: Record<string, unknown>,
|
|
237
|
+
log: Log,
|
|
238
|
+
): ElementInstanceWaitState | undefined {
|
|
239
|
+
const elementInstanceKey = presentEngineKey(row.elementInstanceKey);
|
|
240
|
+
const processInstanceKey = presentEngineKey(row.processInstanceKey);
|
|
241
|
+
const elementId = typeof row.elementId === "string" && row.elementId.trim() !== ""
|
|
242
|
+
? row.elementId.trim()
|
|
243
|
+
: undefined;
|
|
244
|
+
if (elementInstanceKey === undefined || processInstanceKey === undefined || elementId === undefined) {
|
|
245
|
+
log("warn", "skipping element-instance wait state with incomplete identity in engine response");
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
const details = isRecord(row.details) ? row.details : row;
|
|
249
|
+
const waitStateType = normalizeWaitStateType(details.waitStateType ?? row.waitStateType);
|
|
250
|
+
if (waitStateType === undefined) {
|
|
251
|
+
log("warn", "skipping element-instance wait state with unrecognized waitStateType", {
|
|
252
|
+
elementInstanceKey,
|
|
253
|
+
waitStateType: String(details.waitStateType ?? row.waitStateType),
|
|
254
|
+
});
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
const elementType = pickElementType(row);
|
|
258
|
+
const base = {
|
|
259
|
+
elementInstanceKey,
|
|
260
|
+
processInstanceKey,
|
|
261
|
+
elementId,
|
|
262
|
+
...(elementType ? { elementType } : {}),
|
|
263
|
+
};
|
|
264
|
+
switch (waitStateType) {
|
|
265
|
+
case "JOB": {
|
|
266
|
+
// `jobType` is the required discriminator of a JOB park; without it the row is malformed.
|
|
267
|
+
const jobType = presentText(details.jobType);
|
|
268
|
+
if (jobType === undefined) {
|
|
269
|
+
log("warn", "skipping JOB wait state with no jobType in engine response", {
|
|
270
|
+
elementInstanceKey,
|
|
271
|
+
});
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
const jobKey = presentEngineKey(details.jobKey);
|
|
275
|
+
return { ...base, waitStateType, jobType, ...(jobKey ? { jobKey } : {}) };
|
|
276
|
+
}
|
|
277
|
+
case "MESSAGE": {
|
|
278
|
+
// `messageName` is the required discriminator of a MESSAGE park.
|
|
279
|
+
const messageName = presentText(details.messageName);
|
|
280
|
+
if (messageName === undefined) {
|
|
281
|
+
log("warn", "skipping MESSAGE wait state with no messageName in engine response", {
|
|
282
|
+
elementInstanceKey,
|
|
283
|
+
});
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
const correlationKey = presentText(details.correlationKey);
|
|
287
|
+
return {
|
|
288
|
+
...base,
|
|
289
|
+
waitStateType,
|
|
290
|
+
messageName,
|
|
291
|
+
...(correlationKey ? { correlationKey } : {}),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
case "USER_TASK": {
|
|
295
|
+
// `userTaskKey` is the required identity of a USER_TASK park; without it the row is
|
|
296
|
+
// malformed. Skip it (like the missing-identity guards above) rather than emit a typed
|
|
297
|
+
// wait state carrying an empty key that would break a downstream join.
|
|
298
|
+
const userTaskKey = presentEngineKey(details.taskKey ?? details.userTaskKey);
|
|
299
|
+
if (userTaskKey === undefined) {
|
|
300
|
+
log("warn", "skipping USER_TASK wait state with no userTaskKey in engine response", {
|
|
301
|
+
elementInstanceKey,
|
|
302
|
+
});
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
return { ...base, waitStateType, userTaskKey };
|
|
306
|
+
}
|
|
307
|
+
case "SIGNAL": {
|
|
308
|
+
// `signalName` is the required discriminator of a SIGNAL park.
|
|
309
|
+
const signalName = presentText(details.signalName);
|
|
310
|
+
if (signalName === undefined) {
|
|
311
|
+
log("warn", "skipping SIGNAL wait state with no signalName in engine response", {
|
|
312
|
+
elementInstanceKey,
|
|
313
|
+
});
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
return { ...base, waitStateType, signalName };
|
|
317
|
+
}
|
|
318
|
+
case "TIMER":
|
|
319
|
+
return { ...base, waitStateType };
|
|
320
|
+
case "CONDITION":
|
|
321
|
+
return { ...base, waitStateType };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Map one engine incident row onto an {@link IncidentSummary}, or `undefined` when the row is
|
|
326
|
+
* malformed (missing the `incidentKey` or the owning `processInstanceKey` — the two identity
|
|
327
|
+
* fields a caller needs to resolve/act on it). `jobKey`/`elementInstanceKey`/`errorType`/
|
|
328
|
+
* `errorMessage` are best-effort diagnostics: present when the engine reports them, omitted
|
|
329
|
+
* otherwise. The single source of truth `searchIncidents` extracts through. */
|
|
330
|
+
export function mapIncidentRow(
|
|
331
|
+
row: Record<string, unknown>,
|
|
332
|
+
log: Log,
|
|
333
|
+
): IncidentSummary | undefined {
|
|
334
|
+
const incidentKey = presentEngineKey(row.incidentKey);
|
|
335
|
+
if (incidentKey === undefined) {
|
|
336
|
+
log("warn", "skipping incident with no incidentKey in engine response");
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
const processInstanceKey = presentEngineKey(row.processInstanceKey);
|
|
340
|
+
if (processInstanceKey === undefined) {
|
|
341
|
+
log("warn", "skipping incident with no processInstanceKey in engine response", {
|
|
342
|
+
incidentKey,
|
|
343
|
+
});
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
const elementId = presentText(row.elementId);
|
|
347
|
+
const elementInstanceKey = presentEngineKey(row.elementInstanceKey);
|
|
348
|
+
const jobKey = presentEngineKey(row.jobKey);
|
|
349
|
+
const errorType = presentText(row.errorType);
|
|
350
|
+
const errorMessage = presentText(row.errorMessage);
|
|
351
|
+
return {
|
|
352
|
+
incidentKey,
|
|
353
|
+
processInstanceKey,
|
|
354
|
+
...(elementId ? { elementId } : {}),
|
|
355
|
+
...(elementInstanceKey ? { elementInstanceKey } : {}),
|
|
356
|
+
...(jobKey ? { jobKey } : {}),
|
|
357
|
+
...(errorType ? { errorType } : {}),
|
|
358
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
359
|
+
state: normalizeIncidentState(row.state),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
66
363
|
/** A job as delivered to a nano-sdk job handler: the frame fields plus the
|
|
67
364
|
* acknowledgement actions the handler must call. */
|
|
68
365
|
export interface NanoSdkActivatedJob {
|
|
@@ -146,6 +443,38 @@ export interface NanoSdkClient {
|
|
|
146
443
|
consistency?: unknown,
|
|
147
444
|
options?: unknown,
|
|
148
445
|
): Promise<Record<string, unknown>>;
|
|
446
|
+
searchElementInstances(
|
|
447
|
+
input: { filter?: Record<string, unknown>; page?: Record<string, unknown> },
|
|
448
|
+
consistency?: unknown,
|
|
449
|
+
options?: unknown,
|
|
450
|
+
): Promise<Record<string, unknown>>;
|
|
451
|
+
searchElementInstanceWaitStates(
|
|
452
|
+
input: { filter?: Record<string, unknown>; page?: Record<string, unknown> },
|
|
453
|
+
consistency?: unknown,
|
|
454
|
+
options?: unknown,
|
|
455
|
+
): Promise<Record<string, unknown>>;
|
|
456
|
+
getElementInstance(
|
|
457
|
+
input: { elementInstanceKey: string },
|
|
458
|
+
consistency?: unknown,
|
|
459
|
+
options?: unknown,
|
|
460
|
+
): Promise<Record<string, unknown>>;
|
|
461
|
+
searchIncidents(
|
|
462
|
+
input: { filter?: Record<string, unknown>; page?: Record<string, unknown> },
|
|
463
|
+
consistency?: unknown,
|
|
464
|
+
options?: unknown,
|
|
465
|
+
): Promise<Record<string, unknown>>;
|
|
466
|
+
resolveIncident(
|
|
467
|
+
input: { incidentKey: string },
|
|
468
|
+
options?: unknown,
|
|
469
|
+
): Promise<unknown>;
|
|
470
|
+
updateJob(
|
|
471
|
+
input: { jobKey: string; changeset: Record<string, unknown> },
|
|
472
|
+
options?: unknown,
|
|
473
|
+
): Promise<unknown>;
|
|
474
|
+
createElementInstanceVariables(
|
|
475
|
+
input: { elementInstanceKey: string; variables: Record<string, unknown>; local?: boolean },
|
|
476
|
+
options?: unknown,
|
|
477
|
+
): Promise<unknown>;
|
|
149
478
|
createJobWorker(cfg: NanoSdkJobWorkerConfig): NanoSdkJobWorker;
|
|
150
479
|
/** Stop every worker created on this client. Used on teardown to also drain a
|
|
151
480
|
* REST-fallback worker, whose handle the `createJobWorker` proxy starts internally
|
|
@@ -355,6 +684,140 @@ export class SdkEngineClient implements EngineClient {
|
|
|
355
684
|
});
|
|
356
685
|
}
|
|
357
686
|
|
|
687
|
+
async searchElementInstances(
|
|
688
|
+
filter?: ElementInstanceFilter,
|
|
689
|
+
): Promise<ElementInstanceSummary[]> {
|
|
690
|
+
// Mirror `searchProcessInstances`: build the engine filter from the transport-agnostic
|
|
691
|
+
// selectors, read at zero-wait consistency (an element-instance search is eventually
|
|
692
|
+
// consistent), and map each row through the single `mapElementInstanceRow` gate — which
|
|
693
|
+
// also `searchElementInstances`/`getElementInstance` share — dropping malformed rows.
|
|
694
|
+
const f: Record<string, unknown> = {};
|
|
695
|
+
if (filter?.processInstanceKey) f.processInstanceKey = filter.processInstanceKey;
|
|
696
|
+
if (filter?.elementId) f.elementId = filter.elementId;
|
|
697
|
+
if (filter?.state) f.state = filter.state;
|
|
698
|
+
const body = await this.client.searchElementInstances(
|
|
699
|
+
{ filter: f },
|
|
700
|
+
{ consistency: { waitUpToMs: 0 } },
|
|
701
|
+
);
|
|
702
|
+
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
703
|
+
return items.flatMap((it) => {
|
|
704
|
+
const mapped = mapElementInstanceRow(it, this.log);
|
|
705
|
+
return mapped ? [mapped] : [];
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async searchElementInstanceWaitStates(
|
|
710
|
+
filter?: ElementInstanceWaitStateFilter,
|
|
711
|
+
): Promise<ElementInstanceWaitState[]> {
|
|
712
|
+
// The wait-states search surfaces *every* park (job/message/timer/signal/condition/user
|
|
713
|
+
// task), not just user tasks. Same zero-wait read + per-row mapping-gate shape as
|
|
714
|
+
// `searchElementInstances`; the SDK nests park-specific fields under `details`, which
|
|
715
|
+
// `mapElementInstanceWaitStateRow` unwraps.
|
|
716
|
+
const f: Record<string, unknown> = {};
|
|
717
|
+
if (filter?.processInstanceKey) f.processInstanceKey = filter.processInstanceKey;
|
|
718
|
+
if (filter?.elementId) f.elementId = filter.elementId;
|
|
719
|
+
if (filter?.waitStateType) f.waitStateType = filter.waitStateType;
|
|
720
|
+
const body = await this.client.searchElementInstanceWaitStates(
|
|
721
|
+
{ filter: f },
|
|
722
|
+
{ consistency: { waitUpToMs: 0 } },
|
|
723
|
+
);
|
|
724
|
+
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
725
|
+
return items.flatMap((it) => {
|
|
726
|
+
const mapped = mapElementInstanceWaitStateRow(it, this.log);
|
|
727
|
+
return mapped ? [mapped] : [];
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async getElementInstance(
|
|
732
|
+
elementInstanceKey: string,
|
|
733
|
+
): Promise<ElementInstanceSummary | null> {
|
|
734
|
+
// A blank key can never address an element instance — short-circuit to null rather than
|
|
735
|
+
// issue a `GET /element-instances/` with an empty segment. Normalize the key up front (as
|
|
736
|
+
// `getForm` resolves a normalized identifier before fetching) so a padded-but-valid key
|
|
737
|
+
// like `" 5 "` addresses the same element instance rather than 404ing on the raw segment.
|
|
738
|
+
if (typeof elementInstanceKey !== "string" || elementInstanceKey.trim() === "") {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
const key = elementInstanceKey.trim();
|
|
742
|
+
let body: Record<string, unknown>;
|
|
743
|
+
try {
|
|
744
|
+
body = await this.client.getElementInstance(
|
|
745
|
+
{ elementInstanceKey: key },
|
|
746
|
+
{ consistency: { waitUpToMs: 0 } },
|
|
747
|
+
);
|
|
748
|
+
} catch (err) {
|
|
749
|
+
// A 404 (no such element instance) is an expected "not found", not a fault — mirror
|
|
750
|
+
// `getForm`, which treats a failed fetch as absence (null) rather than propagating.
|
|
751
|
+
this.log("warn", "getElementInstance: engine fetch failed", {
|
|
752
|
+
elementInstanceKey: key,
|
|
753
|
+
err: err instanceof Error ? err.message : String(err),
|
|
754
|
+
});
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
if (!isRecord(body)) return null;
|
|
758
|
+
return mapElementInstanceRow(body, this.log) ?? null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async searchIncidents(filter?: IncidentFilter): Promise<IncidentSummary[]> {
|
|
762
|
+
// Same shape as `searchElementInstances`: build the engine filter from the transport-agnostic
|
|
763
|
+
// selectors, read at zero-wait consistency (an incident search is eventually consistent), and
|
|
764
|
+
// map each row through the single `mapIncidentRow` gate, dropping malformed rows.
|
|
765
|
+
const f: Record<string, unknown> = {};
|
|
766
|
+
// Normalize the request selectors the same way the response mapper normalizes keys: a
|
|
767
|
+
// whitespace-only/padded `processInstanceKey` must not reach the engine as a garbage filter
|
|
768
|
+
// segment (which would silently return surprising empty results).
|
|
769
|
+
const processInstanceKey = presentEngineKey(filter?.processInstanceKey);
|
|
770
|
+
if (processInstanceKey) f.processInstanceKey = processInstanceKey;
|
|
771
|
+
if (filter?.state) f.state = filter.state;
|
|
772
|
+
const body = await this.client.searchIncidents(
|
|
773
|
+
{ filter: f },
|
|
774
|
+
{ consistency: { waitUpToMs: 0 } },
|
|
775
|
+
);
|
|
776
|
+
const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
|
|
777
|
+
return items.flatMap((it) => {
|
|
778
|
+
const mapped = mapIncidentRow(it, this.log);
|
|
779
|
+
return mapped ? [mapped] : [];
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async resolveIncident(input: { incidentKey: string }): Promise<void> {
|
|
784
|
+
await this.client.resolveIncident({
|
|
785
|
+
incidentKey: requireEngineKey(input.incidentKey, "incidentKey"),
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async updateJobRetries(input: { jobKey: string; retries: number }): Promise<void> {
|
|
790
|
+
// The "retry a failed job" operation: a job's remaining retries are the `retries` field of
|
|
791
|
+
// the `updateJob` changeset. Bumping them back above zero makes a failed job activatable so a
|
|
792
|
+
// paired `resolveIncident` can return it to the pool.
|
|
793
|
+
const jobKey = requireEngineKey(input.jobKey, "jobKey");
|
|
794
|
+
// `retries` serializes straight into the engine changeset, and `number` admits `NaN`,
|
|
795
|
+
// fractional, and negative values — none of which is a valid retry count. Reject them up
|
|
796
|
+
// front so a tool-driven call fails with a clear message instead of an opaque engine error.
|
|
797
|
+
if (!Number.isInteger(input.retries) || input.retries < 0) {
|
|
798
|
+
throw new Error("retries must be a non-negative integer");
|
|
799
|
+
}
|
|
800
|
+
await this.client.updateJob({
|
|
801
|
+
jobKey,
|
|
802
|
+
changeset: { retries: input.retries },
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async setVariables(input: {
|
|
807
|
+
scopeKey: string;
|
|
808
|
+
variables: Record<string, unknown>;
|
|
809
|
+
local?: boolean;
|
|
810
|
+
}): Promise<void> {
|
|
811
|
+
// A scope is addressed by its element-instance key (the process instance's root scope is an
|
|
812
|
+
// element instance too), so `scopeKey` maps straight onto `elementInstanceKey`. `local`
|
|
813
|
+
// defaults to engine behaviour (propagate to the outermost scope) when unset.
|
|
814
|
+
await this.client.createElementInstanceVariables({
|
|
815
|
+
elementInstanceKey: requireEngineKey(input.scopeKey, "scopeKey"),
|
|
816
|
+
variables: input.variables,
|
|
817
|
+
...(input.local === undefined ? {} : { local: input.local }),
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
358
821
|
async registerWorker(
|
|
359
822
|
jobType: string,
|
|
360
823
|
handler: JobHandler,
|
package/src/runtime/index.ts
CHANGED
|
@@ -83,6 +83,12 @@ export type { ValidationIssue } from "./core/validate.ts";
|
|
|
83
83
|
// Host + engine contracts (for custom hosts / tests)
|
|
84
84
|
export type {
|
|
85
85
|
EngineClient,
|
|
86
|
+
ElementInstanceState,
|
|
87
|
+
ElementInstanceSummary,
|
|
88
|
+
ElementInstanceFilter,
|
|
89
|
+
ElementInstanceWaitState,
|
|
90
|
+
ElementInstanceWaitStateFilter,
|
|
91
|
+
WaitStateType,
|
|
86
92
|
EngineJob,
|
|
87
93
|
FormSchema,
|
|
88
94
|
HostContext,
|
|
@@ -90,6 +96,10 @@ export type {
|
|
|
90
96
|
HttpRequest,
|
|
91
97
|
HttpResponse,
|
|
92
98
|
HttpServer,
|
|
99
|
+
IncidentState,
|
|
100
|
+
EngineIncidentState,
|
|
101
|
+
IncidentFilter,
|
|
102
|
+
IncidentSummary,
|
|
93
103
|
JobHandler,
|
|
94
104
|
ProcessInstanceSnapshot,
|
|
95
105
|
ProcessInstanceState,
|