@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/workflow",
3
- "version": "0.7.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.0"
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 as { cause?: NodeJS.ErrnoException })?.cause;
47
- if (cause?.code !== "ERR_MODULE_NOT_FOUND") throw e;
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<Record<string, unknown>>;
119
+ ): Promise<unknown>;
73
120
  createProcessInstance(
74
121
  input: { processDefinitionId: string; variables?: JsonObject },
75
122
  options?: unknown,
76
- ): Promise<Record<string, unknown>>;
123
+ ): Promise<unknown>;
77
124
  correlateMessage(
78
125
  input: { name: string; correlationKey: string; variables?: JsonObject },
79
126
  options?: unknown,
80
- ): Promise<Record<string, unknown>>;
127
+ ): Promise<unknown>;
81
128
  getProcessInstance(
82
129
  input: { processInstanceKey: string },
83
130
  consistency: { consistency: { waitUpToMs: number } },
84
131
  options?: unknown,
85
- ): Promise<Record<string, unknown>>;
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 err = e as { message?: string; status?: number; response?: { status?: number } };
160
- const status = err?.status ?? err?.response?.status;
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
- this.sdk = createCamundaClient({
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
- }) as unknown as NanoSdkClient;
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] })) as DeployResult;
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
- })) as StartResult;
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
- })) as JsonObject;
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
- )) as JsonObject;
344
+ ));
263
345
  } catch {
264
346
  return null;
265
347
  }