@nanobpm/workflow 0.7.0 → 0.8.1
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 +13 -3
- package/dist/client.d.ts +4 -4
- package/dist/client.js +85 -11
- package/dist/declarative.d.ts +46 -5
- package/dist/declarative.js +280 -28
- package/dist/envelope.js +10 -3
- package/dist/imperative.js +19 -8
- package/dist/layout.js +4 -1
- package/dist/types.d.ts +21 -1
- package/dist/worker.js +18 -8
- package/package.json +3 -3
- package/src/client.ts +102 -20
- package/src/declarative.ts +374 -52
- package/src/envelope.ts +10 -3
- package/src/imperative.ts +20 -9
- package/src/layout.ts +5 -1
- package/src/types.ts +27 -4
- package/src/worker.ts +19 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Code-first durable orchestration for nanobpmn (ADR 0044/0045). Author a durable workflow as a declarative flow — `defineFlow(id, w => { w.run(...); w.signal(...); w.task(...) })` — and the SDK derives the executable BPMN model, the job types, and the message/correlation wiring, then hosts a generic worker. `w.run` is a local step, `w.signal` is a durable human-in-the-loop wait, `w.task` is a step serviced by an external worker. `defineFlow` is the single code-first surface; the imperative `defineWorkflow` replay function is experimental/internal, not a recommended API. No diagram, no task-type wiring, no correlation plumbing authored by hand — and you can optionally auto-generate a diagram (DI) for the derived model with `declarativeToLayoutedBpmn` via the optional `bpmn-auto-layout` peer dependency. Talks to a running nanobpmn gateway through @nanobpm/nano-sdk (the shared engine-transport spine — ADR 0055), which negotiates the Falcon protocol on a Nano server and REST elsewhere.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
},
|
|
29
29
|
"sideEffects": false,
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@nanobpm/nano-sdk": "^1.2.
|
|
31
|
+
"@nanobpm/nano-sdk": "^1.2.3"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsc -p tsconfig.json",
|
|
35
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.types.json",
|
|
36
36
|
"test": "npm run build && npm run test:ci",
|
|
37
37
|
"test:ci": "node --experimental-strip-types --test test/*.test.ts",
|
|
38
38
|
"test:integration": "node --experimental-strip-types --test test/integration.test.ts",
|
package/src/client.ts
CHANGED
|
@@ -4,13 +4,60 @@
|
|
|
4
4
|
// signals; the low-level job protocol used by the Worker runtime is the SDK's
|
|
5
5
|
// own job worker, reached through the exposed `sdk` client.
|
|
6
6
|
|
|
7
|
-
import { createCamundaClient } from "@nanobpm/nano-sdk";
|
|
7
|
+
import { createCamundaClient, JobActionReceiptSymbol, ProcessDefinitionId, ProcessInstanceKey } from "@nanobpm/nano-sdk";
|
|
8
8
|
import { declarativeToBpmn, walkNodes } from "./declarative.js";
|
|
9
9
|
import { imperativeToBpmn } from "./imperative.js";
|
|
10
10
|
import { layoutBpmn } from "./layout.js";
|
|
11
|
-
import type { DeclarativeFlow, DeployResult, Job, JsonObject, StartResult, Workflow } from "./types.js";
|
|
11
|
+
import type { DeclarativeFlow, DeployResult, Job, Json, JsonObject, StartResult, Workflow } from "./types.js";
|
|
12
12
|
import { assertWorkflowIds, messageName } from "./xml.js";
|
|
13
13
|
|
|
14
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
15
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function errorCause(e: unknown): unknown {
|
|
19
|
+
return isRecord(e) ? e.cause : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function errorCode(e: unknown): unknown {
|
|
23
|
+
return isRecord(e) ? e.code : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function errorMessage(e: unknown): string {
|
|
27
|
+
return isRecord(e) && typeof e.message === "string" ? e.message : String(e);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function errorStatus(e: unknown): number | undefined {
|
|
31
|
+
if (!isRecord(e)) return undefined;
|
|
32
|
+
if (typeof e.status === "number") return e.status;
|
|
33
|
+
const response = e.response;
|
|
34
|
+
return isRecord(response) && typeof response.status === "number" ? response.status : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isJson(v: unknown): v is Json {
|
|
38
|
+
if (v === null) return true;
|
|
39
|
+
switch (typeof v) {
|
|
40
|
+
case "boolean":
|
|
41
|
+
case "number":
|
|
42
|
+
case "string":
|
|
43
|
+
return true;
|
|
44
|
+
case "object":
|
|
45
|
+
if (Array.isArray(v)) return v.every(isJson);
|
|
46
|
+
return Object.values(v).every(isJson);
|
|
47
|
+
default:
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toJsonObject(v: unknown): JsonObject {
|
|
53
|
+
if (!isRecord(v)) throw new Error("SDK response was not an object");
|
|
54
|
+
const result: JsonObject = {};
|
|
55
|
+
for (const [key, value] of Object.entries(v)) {
|
|
56
|
+
if (isJson(value)) result[key] = value;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
14
61
|
/** Render a workflow (either surface) to its executable BPMN model. */
|
|
15
62
|
export function toBpmn(wf: Workflow): string {
|
|
16
63
|
assertWorkflowIds(wf);
|
|
@@ -43,8 +90,8 @@ export async function toDeployableBpmn(
|
|
|
43
90
|
// Only fall back for the "optional dep not installed" case (layoutBpmn wraps
|
|
44
91
|
// it with an ERR_MODULE_NOT_FOUND cause). Any other failure is a real layout
|
|
45
92
|
// problem and must surface rather than silently deploy an uninspectable model.
|
|
46
|
-
const cause = (e
|
|
47
|
-
if (cause
|
|
93
|
+
const cause = errorCause(e);
|
|
94
|
+
if (errorCode(cause) !== "ERR_MODULE_NOT_FOUND") throw e;
|
|
48
95
|
if (!warnedNoLayout) {
|
|
49
96
|
warnedNoLayout = true;
|
|
50
97
|
console.warn(
|
|
@@ -69,20 +116,20 @@ export interface NanoSdkClient {
|
|
|
69
116
|
createDeployment(
|
|
70
117
|
input: { resources: File[]; [k: string]: unknown },
|
|
71
118
|
options?: unknown,
|
|
72
|
-
): Promise<
|
|
119
|
+
): Promise<unknown>;
|
|
73
120
|
createProcessInstance(
|
|
74
121
|
input: { processDefinitionId: string; variables?: JsonObject },
|
|
75
122
|
options?: unknown,
|
|
76
|
-
): Promise<
|
|
123
|
+
): Promise<unknown>;
|
|
77
124
|
correlateMessage(
|
|
78
125
|
input: { name: string; correlationKey: string; variables?: JsonObject },
|
|
79
126
|
options?: unknown,
|
|
80
|
-
): Promise<
|
|
127
|
+
): Promise<unknown>;
|
|
81
128
|
getProcessInstance(
|
|
82
129
|
input: { processInstanceKey: string },
|
|
83
130
|
consistency: { consistency: { waitUpToMs: number } },
|
|
84
131
|
options?: unknown,
|
|
85
|
-
): Promise<
|
|
132
|
+
): Promise<unknown>;
|
|
86
133
|
createJobWorker(cfg: JobWorkerConfig): NanoJobWorker;
|
|
87
134
|
}
|
|
88
135
|
|
|
@@ -156,9 +203,8 @@ export class WorkflowError extends Error {
|
|
|
156
203
|
* HTTP status when the SDK attached one. */
|
|
157
204
|
function asWorkflowError(e: unknown, what: string): WorkflowError {
|
|
158
205
|
if (e instanceof WorkflowError) return e;
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
return new WorkflowError(`${what} failed: ${err?.message ?? String(e)}`, status);
|
|
206
|
+
const status = errorStatus(e);
|
|
207
|
+
return new WorkflowError(`${what} failed: ${errorMessage(e)}`, status);
|
|
162
208
|
}
|
|
163
209
|
|
|
164
210
|
export class WorkflowClient {
|
|
@@ -175,13 +221,49 @@ export class WorkflowClient {
|
|
|
175
221
|
return;
|
|
176
222
|
}
|
|
177
223
|
if (!opts?.baseUrl) throw new Error("WorkflowClient needs a baseUrl or a client");
|
|
178
|
-
|
|
224
|
+
const raw = createCamundaClient({
|
|
179
225
|
config: {
|
|
180
226
|
CAMUNDA_REST_ADDRESS: opts.baseUrl.replace(/\/+$/, ""),
|
|
181
227
|
...(opts.token ? { CAMUNDA_TOKEN: opts.token } : {}),
|
|
182
228
|
CAMUNDA_TRANSPORT: opts.transport ?? "auto",
|
|
183
229
|
},
|
|
184
|
-
})
|
|
230
|
+
});
|
|
231
|
+
this.sdk = {
|
|
232
|
+
createDeployment: (input) => raw.createDeployment(input),
|
|
233
|
+
createProcessInstance: (input) =>
|
|
234
|
+
raw.createProcessInstance(
|
|
235
|
+
{
|
|
236
|
+
...input,
|
|
237
|
+
processDefinitionId: ProcessDefinitionId.assumeExists(input.processDefinitionId),
|
|
238
|
+
},
|
|
239
|
+
),
|
|
240
|
+
correlateMessage: (input) => raw.correlateMessage(input),
|
|
241
|
+
getProcessInstance: (input, consistency) =>
|
|
242
|
+
raw.getProcessInstance(
|
|
243
|
+
{
|
|
244
|
+
processInstanceKey: ProcessInstanceKey.assumeExists(input.processInstanceKey),
|
|
245
|
+
},
|
|
246
|
+
consistency,
|
|
247
|
+
),
|
|
248
|
+
createJobWorker: (cfg) => {
|
|
249
|
+
const worker = raw.createJobWorker({
|
|
250
|
+
...cfg,
|
|
251
|
+
jobHandler: async (job) => {
|
|
252
|
+
await cfg.jobHandler(job);
|
|
253
|
+
return JobActionReceiptSymbol;
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
return {
|
|
257
|
+
start: () => {
|
|
258
|
+
void worker.start();
|
|
259
|
+
},
|
|
260
|
+
stop: () => worker.stop(),
|
|
261
|
+
stopGracefully: async (opts) => {
|
|
262
|
+
await worker.stopGracefully(opts);
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
};
|
|
185
267
|
}
|
|
186
268
|
|
|
187
269
|
/**
|
|
@@ -195,7 +277,7 @@ export class WorkflowClient {
|
|
|
195
277
|
const xml = await toDeployableBpmn(wf, opts);
|
|
196
278
|
const file = new File([xml], `${wf.id}.bpmn`, { type: "text/xml" });
|
|
197
279
|
try {
|
|
198
|
-
return (await this.sdk.createDeployment({ resources: [file] }))
|
|
280
|
+
return toJsonObject(await this.sdk.createDeployment({ resources: [file] }));
|
|
199
281
|
} catch (e) {
|
|
200
282
|
throw asWorkflowError(e, "deploy");
|
|
201
283
|
}
|
|
@@ -210,10 +292,10 @@ export class WorkflowClient {
|
|
|
210
292
|
const variables: JsonObject =
|
|
211
293
|
wf.kind === "imperative" ? { input, journal: {}, wfDone: false } : input;
|
|
212
294
|
try {
|
|
213
|
-
return (await this.sdk.createProcessInstance({
|
|
295
|
+
return toJsonObject(await this.sdk.createProcessInstance({
|
|
214
296
|
processDefinitionId: wf.id,
|
|
215
297
|
variables,
|
|
216
|
-
}))
|
|
298
|
+
}));
|
|
217
299
|
} catch (e) {
|
|
218
300
|
throw asWorkflowError(e, "start");
|
|
219
301
|
}
|
|
@@ -242,11 +324,11 @@ export class WorkflowClient {
|
|
|
242
324
|
);
|
|
243
325
|
}
|
|
244
326
|
try {
|
|
245
|
-
return (await this.sdk.correlateMessage({
|
|
327
|
+
return toJsonObject(await this.sdk.correlateMessage({
|
|
246
328
|
name: messageName(flow.id, signalName),
|
|
247
329
|
correlationKey,
|
|
248
330
|
variables,
|
|
249
|
-
}))
|
|
331
|
+
}));
|
|
250
332
|
} catch (e) {
|
|
251
333
|
throw asWorkflowError(e, `signal "${signalName}"`);
|
|
252
334
|
}
|
|
@@ -256,10 +338,10 @@ export class WorkflowClient {
|
|
|
256
338
|
* zero-wait consistency; returns null when the instance is not (yet) visible. */
|
|
257
339
|
async getInstance(processInstanceKey: string): Promise<JsonObject | null> {
|
|
258
340
|
try {
|
|
259
|
-
return (await this.sdk.getProcessInstance(
|
|
341
|
+
return toJsonObject(await this.sdk.getProcessInstance(
|
|
260
342
|
{ processInstanceKey },
|
|
261
343
|
{ consistency: { waitUpToMs: 0 } },
|
|
262
|
-
))
|
|
344
|
+
));
|
|
263
345
|
} catch {
|
|
264
346
|
return null;
|
|
265
347
|
}
|