@nanobpm/workflow 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/dist/worker.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // The Worker runtime — hosts one or more workflows against a gateway and drives
2
- // them: long-polls each derived job type, dispatches jobs to handlers, completes
3
- // them. It generalises the two surfaces:
2
+ // them: it registers one nano-sdk job worker per derived job type, dispatches
3
+ // each activated job to its handler, and completes it. It generalises the two
4
+ // surfaces:
4
5
  //
5
6
  // - a declarative flow contributes one job type per `run` step, dispatched to
6
7
  // the user handler (external `task` steps are intentionally NOT hosted here —
@@ -8,32 +9,34 @@
8
9
  // - an imperative workflow contributes its single orchestrator job type,
9
10
  // dispatched to the replay engine (which advances the journal one step).
10
11
  //
11
- // It is resilient to the gateway disappearing (each poll loop backs off and
12
- // reconnects on its own), so it survives an engine crash/restart — the property
13
- // the ADR 0044 spike proved.
12
+ // Transport (activation, completion, failure, long-poll, backoff/reconnect) is
13
+ // the nano-sdk job worker (ADR 0055), reached through `WorkflowClient.sdk`. The
14
+ // nano-sdk worker reschedules its poll on an activation error, so the runtime
15
+ // survives an engine crash/restart — the property the ADR 0044 spike proved.
14
16
  import { WorkflowClient } from "./client.js";
15
17
  import { replayOnce } from "./imperative.js";
16
18
  import { walkNodes } from "./declarative.js";
17
19
  import { jobType } from "./xml.js";
18
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
19
20
  export class Worker {
20
21
  client;
21
22
  name;
22
23
  pollTimeoutMs;
23
24
  jobTimeoutMs;
24
- backoffMs;
25
+ maxParallelJobs;
25
26
  onActivity;
26
27
  onError;
27
28
  /** job type → { workflowId, handle } */
28
29
  routes = new Map();
29
30
  running = false;
30
- loops = [];
31
+ workers = [];
31
32
  constructor(opts) {
32
- this.client = opts.client ?? new WorkflowClient({ baseUrl: requireBaseUrl(opts) });
33
+ this.client =
34
+ opts.client ??
35
+ new WorkflowClient({ baseUrl: requireBaseUrl(opts), transport: opts.transport ?? "rest" });
33
36
  this.name = opts.name ?? "nanobpm-workflow";
34
37
  this.pollTimeoutMs = opts.pollTimeoutMs ?? 10000;
35
38
  this.jobTimeoutMs = opts.jobTimeoutMs ?? 30000;
36
- this.backoffMs = opts.backoffMs ?? 500;
39
+ this.maxParallelJobs = opts.maxParallelJobs ?? 1;
37
40
  this.onActivity = opts.onActivity;
38
41
  this.onError = opts.onError;
39
42
  for (const wf of opts.workflows)
@@ -114,64 +117,80 @@ export class Worker {
114
117
  get servedTypes() {
115
118
  return [...this.routes.keys()];
116
119
  }
117
- /** Begin polling. Resolves once the loops are running (they run until stop()). */
120
+ /** Begin serving. Creates one nano-sdk job worker per derived job type with
121
+ * `autoStart: false`, then starts each explicitly, so the poll lifecycle is
122
+ * deterministic and owned here rather than left to the SDK's auto-start
123
+ * default (matching the `JobWorkerConfig.autoStart` contract). Nothing polls
124
+ * before `start()` is called. */
118
125
  start() {
119
126
  if (this.running)
120
127
  return;
121
128
  this.running = true;
122
- this.loops = [...this.routes.entries()].map(([type, route]) => this.pollLoop(type, route));
129
+ this.workers = [...this.routes.entries()].map(([type, route]) => {
130
+ const worker = this.client.sdk.createJobWorker({
131
+ jobType: type,
132
+ workerName: this.name,
133
+ maxParallelJobs: this.maxParallelJobs,
134
+ jobTimeoutMs: this.jobTimeoutMs,
135
+ pollTimeoutMs: this.pollTimeoutMs,
136
+ autoStart: false,
137
+ jobHandler: (job) => this.handleJob(type, route, job),
138
+ });
139
+ worker.start();
140
+ return worker;
141
+ });
123
142
  }
124
- /** Stop polling and wait for in-flight loops to unwind (≤ pollTimeoutMs). */
143
+ /** Stop serving and wait for in-flight jobs to drain. Stopping is best-effort
144
+ * per worker: the Falcon worker's `stop` throws if it never bound a transport
145
+ * (detection still pending), which must not fail the whole shutdown. */
125
146
  async stop() {
126
147
  this.running = false;
127
- await Promise.all(this.loops);
128
- this.loops = [];
129
- }
130
- async pollLoop(type, route) {
131
- while (this.running) {
132
- let jobs = [];
148
+ await Promise.all(this.workers.map(async (w) => {
133
149
  try {
134
- jobs = await this.client.activateJobs(type, {
135
- worker: this.name,
136
- maxJobsToActivate: 1,
137
- timeout: this.jobTimeoutMs,
138
- requestTimeout: this.pollTimeoutMs,
139
- });
150
+ await (w.stopGracefully
151
+ ? w.stopGracefully({ waitUpToMs: this.pollTimeoutMs })
152
+ : w.stop());
140
153
  }
141
- catch (e) {
142
- // Transport/gateway error (e.g. engine restarting): back off, reconnect.
143
- this.emitError(e, type);
144
- await sleep(this.backoffMs);
145
- continue;
154
+ catch {
155
+ /* worker never fully started (transport unbound); nothing to drain */
146
156
  }
147
- for (const job of jobs) {
148
- if (!this.running)
149
- break;
150
- try {
151
- const { variables, step } = await route.handle(job);
152
- await this.client.completeJob(job.jobKey, variables);
153
- // Job is now completed; the observer hook is isolated (emitActivity
154
- // swallows its own errors) so it can never fall into the failure path.
155
- await this.emitActivity({
156
- workflowId: route.workflowId,
157
- type,
158
- jobKey: job.jobKey,
159
- elementId: job.elementId,
160
- step,
161
- });
162
- }
163
- catch (e) {
164
- // A handler or completion failed. Report; the engine will redeliver the
165
- // job after its lock times out (at-least-once → handlers must be
166
- // idempotent). Best-effort surface it as an incident-worthy failure.
167
- this.emitError(e, type);
168
- try {
169
- await this.client.failJob(job.jobKey, e.message, 0);
170
- }
171
- catch {
172
- /* engine may be down; the lock will expire and redeliver */
173
- }
174
- }
157
+ }));
158
+ this.workers = [];
159
+ }
160
+ /** Dispatch one activated job to its route handler and acknowledge it. Returns
161
+ * the nano-sdk action receipt (its `complete`/`fail` result). */
162
+ async handleJob(type, route, job) {
163
+ try {
164
+ const { variables, step } = await route.handle(job);
165
+ const receipt = await job.complete(variables);
166
+ // Job is now completed; the observer hook is isolated (emitActivity
167
+ // swallows its own errors) so it can never fall into the failure path.
168
+ await this.emitActivity({
169
+ workflowId: route.workflowId,
170
+ type,
171
+ jobKey: job.jobKey,
172
+ elementId: job.elementId,
173
+ step,
174
+ });
175
+ return receipt;
176
+ }
177
+ catch (e) {
178
+ // A handler or completion failed. Report; the engine will redeliver the
179
+ // job after its lock times out (at-least-once → handlers must be
180
+ // idempotent). Best-effort surface it as an incident-worthy failure.
181
+ // Normalise non-Error throws (strings, objects) so emitError always gets
182
+ // an Error and errorMessage is never undefined in the failure report.
183
+ const err = e instanceof Error ? e : new Error(String(e));
184
+ this.emitError(err, type);
185
+ try {
186
+ return await job.fail({ errorMessage: err.message, retries: 0 });
187
+ }
188
+ catch {
189
+ // Even reporting the failure failed (e.g. the gateway is unreachable).
190
+ // Awaited + swallowed so a rejected `fail` can't escape as an unhandled
191
+ // rejection out of the SDK worker's handler; the engine redelivers the
192
+ // job once its lock times out regardless.
193
+ return undefined;
175
194
  }
176
195
  }
177
196
  }
package/dist/xml.d.ts CHANGED
@@ -8,4 +8,7 @@ export declare const messageName: (workflowId: string, step: string) => string;
8
8
  export declare const orchestrateType: (workflowId: string) => string;
9
9
  export declare function assertIdent(kind: string, value: string): void;
10
10
  export declare function assertJobType(kind: string, value: string): void;
11
+ export declare function assertTimerDuration(kind: string, value: string): void;
12
+ export declare function assertTimerCycle(kind: string, value: string): void;
13
+ export declare function assertTimerDate(kind: string, value: string): void;
11
14
  export declare function assertWorkflowIds(wf: Workflow): void;
package/dist/xml.js CHANGED
@@ -29,6 +29,56 @@ export function assertJobType(kind, value) {
29
29
  throw new Error(`${kind} "${value}" is not a valid job type (expected a non-empty token of letters, digits, and _ . : + -)`);
30
30
  }
31
31
  }
32
+ // --- Timer definitions (BPMN timerEventDefinition bodies) --------------------
33
+ //
34
+ // The engine (engine-core/src/bpmn.rs) accepts a nested `timeDuration`,
35
+ // `timeCycle`, or `timeDate` as either a static ISO-8601 literal or a FEEL
36
+ // expression (a leading `=`). We validate the literal forms at authoring time so
37
+ // a malformed timer fails fast in the DSL rather than at deploy.
38
+ /** An ISO-8601 duration with integer week/day/hour/minute/second components
39
+ * (no years/months — ambiguous length — matching the engine's parser). */
40
+ const ISO_DURATION = /^P(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?$/;
41
+ function isIsoDuration(v) {
42
+ if (!ISO_DURATION.test(v) || v === "P" || v === "PT" || v.endsWith("T"))
43
+ return false;
44
+ return /\d/.test(v);
45
+ }
46
+ /** A recurring ISO-8601 interval: an optional `R[n]/` repeat prefix + a
47
+ * duration, or a bare duration (the engine repeats it unboundedly). */
48
+ function isIsoCycle(v) {
49
+ const slash = v.indexOf("/");
50
+ if (slash < 0)
51
+ return isIsoDuration(v);
52
+ const repeat = v.slice(0, slash);
53
+ const interval = v.slice(slash + 1);
54
+ return /^R\d*$/.test(repeat) && isIsoDuration(interval);
55
+ }
56
+ /** An ISO-8601 date-time instant (a `timeDate`). A `timeDate` is an absolute
57
+ * instant, so a zone designator (`Z` or an explicit `±HH:MM` offset) is
58
+ * required — a bare local datetime like `2026-01-01T09:00:00` is ambiguous. */
59
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
60
+ /** A FEEL expression body (a leading `=`, non-empty). */
61
+ function isFeel(v) {
62
+ return v.startsWith("=") && v.length > 1;
63
+ }
64
+ export function assertTimerDuration(kind, value) {
65
+ const v = typeof value === "string" ? value.trim() : "";
66
+ if (v === "" || (!isFeel(v) && !isIsoDuration(v))) {
67
+ throw new Error(`${kind} "${value}" is not an ISO-8601 duration (e.g. PT1M30S, P1DT6H) or a FEEL expression (=...)`);
68
+ }
69
+ }
70
+ export function assertTimerCycle(kind, value) {
71
+ const v = typeof value === "string" ? value.trim() : "";
72
+ if (v === "" || (!isFeel(v) && !isIsoCycle(v))) {
73
+ throw new Error(`${kind} "${value}" is not an ISO-8601 repeating interval (e.g. R/PT1H, R5/PT30M), a bare duration (e.g. PT1H), or a FEEL expression (=...)`);
74
+ }
75
+ }
76
+ export function assertTimerDate(kind, value) {
77
+ const v = typeof value === "string" ? value.trim() : "";
78
+ if (v === "" || (!isFeel(v) && !ISO_DATE.test(v))) {
79
+ throw new Error(`${kind} "${value}" is not an ISO-8601 instant (e.g. 2026-01-01T09:00:00Z) or a FEEL expression (=...)`);
80
+ }
81
+ }
32
82
  export function assertWorkflowIds(wf) {
33
83
  assertIdent("workflow id", wf.id);
34
84
  if (wf.kind === "declarative") {
@@ -42,6 +92,9 @@ function assertNodeNames(nodes) {
42
92
  case "signal":
43
93
  assertIdent("step name", node.name);
44
94
  break;
95
+ case "timer":
96
+ assertIdent("step name", node.name);
97
+ break;
45
98
  case "task":
46
99
  assertIdent("step name", node.name);
47
100
  if (node.jobType !== undefined)
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nanobpm/workflow",
3
- "version": "0.5.0",
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 over the REST v2 API.",
3
+ "version": "0.7.0",
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",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/jwulf/nano-ide.git",
9
+ "url": "git+https://github.com/jwulf/nano-ide.git",
10
10
  "directory": "packages/workflow"
11
11
  },
12
12
  "main": "./dist/index.js",
@@ -24,9 +24,12 @@
24
24
  "src"
25
25
  ],
26
26
  "engines": {
27
- "node": ">=20"
27
+ "node": ">=22.6"
28
28
  },
29
29
  "sideEffects": false,
30
+ "dependencies": {
31
+ "@nanobpm/nano-sdk": "^1.2.0"
32
+ },
30
33
  "scripts": {
31
34
  "build": "tsc -p tsconfig.json",
32
35
  "typecheck": "tsc -p tsconfig.json --noEmit",
package/src/client.ts CHANGED
@@ -1,7 +1,10 @@
1
- // A thin, dependency-free client for a running nanobpmn gateway (REST v2):
2
- // deploy a workflow's derived model, start instances, correlate signals, and the
3
- // low-level job activate/complete/fail used by the Worker runtime.
1
+ // A client for a running nanobpmn gateway, built on `@nanobpm/nano-sdk` (the
2
+ // single engine-transport spine — ADR 0055). It derives a workflow's model,
3
+ // deploys it (with diagram interchange), starts instances, and correlates
4
+ // signals; the low-level job protocol used by the Worker runtime is the SDK's
5
+ // own job worker, reached through the exposed `sdk` client.
4
6
 
7
+ import { createCamundaClient } from "@nanobpm/nano-sdk";
5
8
  import { declarativeToBpmn, walkNodes } from "./declarative.js";
6
9
  import { imperativeToBpmn } from "./imperative.js";
7
10
  import { layoutBpmn } from "./layout.js";
@@ -55,24 +58,89 @@ export async function toDeployableBpmn(
55
58
  }
56
59
  }
57
60
 
58
- type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
59
61
 
60
- export interface WorkflowClientOptions {
61
- /** Base URL of the nanobpmn gateway, e.g. `http://localhost:8080`. */
62
- baseUrl: string;
63
- /** Injectable fetch (defaults to the global). Useful for tests/proxies. */
64
- fetch?: FetchLike;
62
+ /**
63
+ * The subset of the `@nanobpm/nano-sdk` (Camunda orchestration-cluster) client
64
+ * that the workflow surface uses. `createCamundaClient` returns a superset of
65
+ * this, so a test — or an author bringing their own transport (e.g. the embedded
66
+ * engine) — can inject any object satisfying it.
67
+ */
68
+ export interface NanoSdkClient {
69
+ createDeployment(
70
+ input: { resources: File[]; [k: string]: unknown },
71
+ options?: unknown,
72
+ ): Promise<Record<string, unknown>>;
73
+ createProcessInstance(
74
+ input: { processDefinitionId: string; variables?: JsonObject },
75
+ options?: unknown,
76
+ ): Promise<Record<string, unknown>>;
77
+ correlateMessage(
78
+ input: { name: string; correlationKey: string; variables?: JsonObject },
79
+ options?: unknown,
80
+ ): Promise<Record<string, unknown>>;
81
+ getProcessInstance(
82
+ input: { processInstanceKey: string },
83
+ consistency: { consistency: { waitUpToMs: number } },
84
+ options?: unknown,
85
+ ): Promise<Record<string, unknown>>;
86
+ createJobWorker(cfg: JobWorkerConfig): NanoJobWorker;
65
87
  }
66
88
 
67
- export interface ActivateOptions {
68
- worker: string;
69
- maxJobsToActivate?: number;
70
- /** Job timeout in ms (how long the worker holds the job). Default 30000. */
71
- timeout?: number;
72
- /** Long-poll timeout in ms; `< 0` disables long-poll. Default 15000. */
73
- requestTimeout?: number;
89
+ /** Config for a nano-sdk job worker (the subset the Worker runtime sets). */
90
+ export interface JobWorkerConfig {
91
+ jobType: string;
92
+ jobHandler: (job: ActivatedJob) => Promise<unknown> | unknown;
93
+ workerName?: string;
94
+ maxParallelJobs?: number;
95
+ /** Job lock timeout in ms. */
96
+ jobTimeoutMs?: number;
97
+ /** Long-poll timeout in ms. */
98
+ pollTimeoutMs?: number;
99
+ /** Start polling immediately. The Worker runtime sets this false and starts explicitly. */
100
+ autoStart?: boolean;
74
101
  }
75
102
 
103
+ /** The handle returned by `createJobWorker`. */
104
+ export interface NanoJobWorker {
105
+ start(): void;
106
+ stop(): void | Promise<void>;
107
+ stopGracefully?(opts?: { waitUpToMs?: number }): Promise<void>;
108
+ }
109
+
110
+ /** An activated job as delivered to a nano-sdk job handler: the workflow `Job`
111
+ * fields plus the acknowledgement actions. */
112
+ export type ActivatedJob = Job & {
113
+ complete(variables?: JsonObject): Promise<unknown>;
114
+ fail(body: { errorMessage: string; retries?: number }): Promise<unknown>;
115
+ };
116
+
117
+ /** Options common to both ways of building a `WorkflowClient`. */
118
+ interface WorkflowClientCommon {
119
+ /** Auth token for the gateway (CAMUNDA_TOKEN). */
120
+ token?: string;
121
+ /** Transport mode passed to `createCamundaClient`: "auto" | "falcon" | "rest".
122
+ * Default "auto" (Falcon on a Nano server, REST elsewhere). */
123
+ transport?: "auto" | "falcon" | "rest";
124
+ }
125
+
126
+ /** Construct a `WorkflowClient` from **either** a `baseUrl` (a nano-sdk client is
127
+ * built for you) **or** a pre-built `client`. The union makes TypeScript enforce
128
+ * that exactly one is supplied, matching the constructor's runtime requirement. */
129
+ export type WorkflowClientOptions =
130
+ | (WorkflowClientCommon & {
131
+ /** Base URL of the nanobpmn gateway, e.g. `http://localhost:8080`. The
132
+ * nano-sdk client normalises this to the `/v2` REST address. */
133
+ baseUrl: string;
134
+ client?: never;
135
+ })
136
+ | (WorkflowClientCommon & {
137
+ /** Inject a pre-built nano-sdk client (or a compatible fake) instead of
138
+ * constructing one from `baseUrl`. Useful for tests, the embedded
139
+ * transport, and advanced authors who build the client themselves. */
140
+ client: NanoSdkClient;
141
+ baseUrl?: never;
142
+ });
143
+
76
144
  export class WorkflowError extends Error {
77
145
  constructor(
78
146
  message: string,
@@ -84,37 +152,36 @@ export class WorkflowError extends Error {
84
152
  }
85
153
  }
86
154
 
155
+ /** Normalise a thrown SDK/transport error into a WorkflowError, preserving an
156
+ * HTTP status when the SDK attached one. */
157
+ function asWorkflowError(e: unknown, what: string): WorkflowError {
158
+ 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);
162
+ }
163
+
87
164
  export class WorkflowClient {
88
- private readonly baseUrl: string;
89
- private readonly fetchImpl: FetchLike;
165
+ /** The underlying nano-sdk client, exposed so the Worker runtime and app
166
+ * authors can reach the engine through the same transport (ADR 0055). The
167
+ * exported `NanoSdkClient` type intentionally models only the subset of
168
+ * methods this package uses (deploy, create, correlate, get, job workers);
169
+ * the runtime value is the full nano-sdk client. */
170
+ readonly sdk: NanoSdkClient;
90
171
 
91
172
  constructor(opts: WorkflowClientOptions) {
92
- if (!opts?.baseUrl) throw new Error("WorkflowClient needs a baseUrl");
93
- this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
94
- const f = opts.fetch ?? (globalThis.fetch as FetchLike | undefined);
95
- if (!f) throw new Error("no fetch available; pass options.fetch (Node < 18)");
96
- this.fetchImpl = f;
97
- }
98
-
99
- private async json<T>(path: string, init: RequestInit, what: string): Promise<T> {
100
- const res = await this.send(path, init, what);
101
- return (await res.json()) as T;
102
- }
103
-
104
- /** Perform a request, throwing WorkflowError on transport error or !ok, and
105
- * return the raw Response (for endpoints with an empty/no-content body). */
106
- private async send(path: string, init: RequestInit, what: string): Promise<Response> {
107
- let res: Response;
108
- try {
109
- res = await this.fetchImpl(`${this.baseUrl}${path}`, init);
110
- } catch (e) {
111
- throw new WorkflowError(`${what} transport error: ${(e as Error).message}`);
112
- }
113
- if (!res.ok) {
114
- const body = await res.text().catch(() => "");
115
- throw new WorkflowError(`${what} failed: ${res.status}`, res.status, body);
173
+ if (opts?.client) {
174
+ this.sdk = opts.client;
175
+ return;
116
176
  }
117
- return res;
177
+ if (!opts?.baseUrl) throw new Error("WorkflowClient needs a baseUrl or a client");
178
+ this.sdk = createCamundaClient({
179
+ config: {
180
+ CAMUNDA_REST_ADDRESS: opts.baseUrl.replace(/\/+$/, ""),
181
+ ...(opts.token ? { CAMUNDA_TOKEN: opts.token } : {}),
182
+ CAMUNDA_TRANSPORT: opts.transport ?? "auto",
183
+ },
184
+ }) as unknown as NanoSdkClient;
118
185
  }
119
186
 
120
187
  /**
@@ -126,9 +193,12 @@ export class WorkflowClient {
126
193
  */
127
194
  async deploy(wf: Workflow, opts: { layout?: boolean } = {}): Promise<DeployResult> {
128
195
  const xml = await toDeployableBpmn(wf, opts);
129
- const form = new FormData();
130
- form.append("resources", new Blob([xml], { type: "text/xml" }), `${wf.id}.bpmn`);
131
- return this.json<DeployResult>(`/v2/deployments`, { method: "POST", body: form }, "deploy");
196
+ const file = new File([xml], `${wf.id}.bpmn`, { type: "text/xml" });
197
+ try {
198
+ return (await this.sdk.createDeployment({ resources: [file] })) as DeployResult;
199
+ } catch (e) {
200
+ throw asWorkflowError(e, "deploy");
201
+ }
132
202
  }
133
203
 
134
204
  /**
@@ -139,15 +209,14 @@ export class WorkflowClient {
139
209
  async start(wf: Workflow, input: JsonObject = {}): Promise<StartResult> {
140
210
  const variables: JsonObject =
141
211
  wf.kind === "imperative" ? { input, journal: {}, wfDone: false } : input;
142
- return this.json<StartResult>(
143
- `/v2/process-instances`,
144
- {
145
- method: "POST",
146
- headers: { "content-type": "application/json" },
147
- body: JSON.stringify({ processDefinitionId: wf.id, variables }),
148
- },
149
- "start",
150
- );
212
+ try {
213
+ return (await this.sdk.createProcessInstance({
214
+ processDefinitionId: wf.id,
215
+ variables,
216
+ })) as StartResult;
217
+ } catch (e) {
218
+ throw asWorkflowError(e, "start");
219
+ }
151
220
  }
152
221
 
153
222
  /** Correlate a signal to a parked declarative `signal` step. Fails fast on an
@@ -172,59 +241,27 @@ export class WorkflowClient {
172
241
  }`,
173
242
  );
174
243
  }
175
- return this.json<JsonObject>(
176
- `/v2/messages/correlation`,
177
- {
178
- method: "POST",
179
- headers: { "content-type": "application/json" },
180
- body: JSON.stringify({ name: messageName(flow.id, signalName), correlationKey, variables }),
181
- },
182
- `signal "${signalName}"`,
183
- );
244
+ try {
245
+ return (await this.sdk.correlateMessage({
246
+ name: messageName(flow.id, signalName),
247
+ correlationKey,
248
+ variables,
249
+ })) as JsonObject;
250
+ } catch (e) {
251
+ throw asWorkflowError(e, `signal "${signalName}"`);
252
+ }
184
253
  }
185
254
 
186
- /** Fetch an instance (used by demos/tests to observe completion). */
255
+ /** Fetch an instance (used by demos/tests to observe completion). Reads with
256
+ * zero-wait consistency; returns null when the instance is not (yet) visible. */
187
257
  async getInstance(processInstanceKey: string): Promise<JsonObject | null> {
188
258
  try {
189
- const res = await this.fetchImpl(`${this.baseUrl}/v2/process-instances/${processInstanceKey}`);
190
- if (!res.ok) return null;
191
- return (await res.json()) as JsonObject;
259
+ return (await this.sdk.getProcessInstance(
260
+ { processInstanceKey },
261
+ { consistency: { waitUpToMs: 0 } },
262
+ )) as JsonObject;
192
263
  } catch {
193
264
  return null;
194
265
  }
195
266
  }
196
-
197
- // --- low-level job protocol (used by the Worker runtime) -------------------
198
-
199
- async activateJobs(type: string, opts: ActivateOptions): Promise<Job[]> {
200
- const body = {
201
- type,
202
- worker: opts.worker,
203
- maxJobsToActivate: opts.maxJobsToActivate ?? 1,
204
- timeout: opts.timeout ?? 30000,
205
- requestTimeout: opts.requestTimeout ?? 15000,
206
- };
207
- const res = await this.json<{ jobs?: Job[] }>(
208
- `/v2/jobs/activation`,
209
- { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) },
210
- `activate ${type}`,
211
- );
212
- return res.jobs ?? [];
213
- }
214
-
215
- async completeJob(jobKey: string, variables: JsonObject = {}): Promise<void> {
216
- await this.send(
217
- `/v2/jobs/${jobKey}/completion`,
218
- { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ variables }) },
219
- "complete job",
220
- );
221
- }
222
-
223
- async failJob(jobKey: string, errorMessage: string, retries = 0): Promise<void> {
224
- await this.send(
225
- `/v2/jobs/${jobKey}/failure`,
226
- { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ errorMessage, retries }) },
227
- "fail job",
228
- );
229
- }
230
267
  }