@nanobpm/urban 0.3.0 → 0.5.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.
Files changed (71) hide show
  1. package/README.md +17 -9
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +212 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.js +25 -0
  6. package/dist/runtime/adapters/deno.d.ts +6 -0
  7. package/dist/runtime/adapters/deno.js +98 -0
  8. package/dist/runtime/adapters/detect.d.ts +9 -0
  9. package/dist/runtime/adapters/detect.js +13 -0
  10. package/dist/runtime/adapters/node.d.ts +7 -0
  11. package/dist/runtime/adapters/node.js +113 -0
  12. package/dist/runtime/core/context.d.ts +40 -0
  13. package/dist/runtime/core/context.js +1 -0
  14. package/dist/runtime/core/glob.d.ts +5 -0
  15. package/dist/runtime/core/glob.js +51 -0
  16. package/dist/runtime/core/host.d.ts +128 -0
  17. package/dist/runtime/core/host.js +5 -0
  18. package/dist/runtime/core/manifest.d.ts +83 -0
  19. package/dist/runtime/core/manifest.js +45 -0
  20. package/dist/runtime/core/modules/datasource.d.ts +38 -0
  21. package/dist/runtime/core/modules/datasource.js +172 -0
  22. package/dist/runtime/core/modules/deploy.d.ts +6 -0
  23. package/dist/runtime/core/modules/deploy.js +37 -0
  24. package/dist/runtime/core/modules/security.d.ts +11 -0
  25. package/dist/runtime/core/modules/security.js +32 -0
  26. package/dist/runtime/core/modules/surfaces.d.ts +9 -0
  27. package/dist/runtime/core/modules/surfaces.js +88 -0
  28. package/dist/runtime/core/modules/triggers.d.ts +15 -0
  29. package/dist/runtime/core/modules/triggers.js +119 -0
  30. package/dist/runtime/core/modules/workers.d.ts +16 -0
  31. package/dist/runtime/core/modules/workers.js +79 -0
  32. package/dist/runtime/core/router.d.ts +22 -0
  33. package/dist/runtime/core/router.js +39 -0
  34. package/dist/runtime/core/runtime.d.ts +46 -0
  35. package/dist/runtime/core/runtime.js +173 -0
  36. package/dist/runtime/core/validate.d.ts +13 -0
  37. package/dist/runtime/core/validate.js +108 -0
  38. package/dist/runtime/engine/nanosdk.d.ts +151 -0
  39. package/dist/runtime/engine/nanosdk.js +209 -0
  40. package/dist/runtime/engine/sdk.d.ts +3 -0
  41. package/dist/runtime/engine/sdk.js +13 -0
  42. package/dist/runtime/index.d.ts +21 -0
  43. package/dist/runtime/index.js +17 -0
  44. package/dist/runtime/run.d.ts +17 -0
  45. package/dist/runtime/run.js +69 -0
  46. package/dist/runtime/schema/nano-app.schema.json +471 -0
  47. package/dist/toolkit/artifact.d.ts +19 -0
  48. package/dist/toolkit/artifact.js +11 -0
  49. package/dist/toolkit/derivers/migrations.d.ts +26 -0
  50. package/dist/toolkit/derivers/migrations.js +73 -0
  51. package/dist/toolkit/derivers/worker-io.d.ts +38 -0
  52. package/dist/toolkit/derivers/worker-io.js +135 -0
  53. package/dist/toolkit/fsio.d.ts +2 -0
  54. package/dist/toolkit/fsio.js +35 -0
  55. package/dist/toolkit/gen.d.ts +28 -0
  56. package/dist/toolkit/gen.js +75 -0
  57. package/dist/toolkit/index.d.ts +13 -0
  58. package/dist/toolkit/index.js +15 -0
  59. package/package.json +20 -6
  60. package/src/bin.mjs +6 -11
  61. package/src/cli.ts +14 -1
  62. package/src/index.ts +7 -0
  63. package/src/runtime/core/context.ts +10 -0
  64. package/src/runtime/core/runtime.test.ts +46 -1
  65. package/src/runtime/core/runtime.ts +9 -0
  66. package/src/runtime/engine/nanosdk.test.ts +245 -85
  67. package/src/runtime/engine/nanosdk.ts +284 -78
  68. package/src/runtime/engine/sdk.ts +18 -0
  69. package/src/runtime/index.ts +8 -4
  70. package/src/runtime/run.ts +15 -14
  71. package/src/runtime/engine/rest.ts +0 -243
@@ -1,19 +1,76 @@
1
- // nano-sdk engine adapter. The console wires every generated app to `@nanobpm/nano-sdk`'s
2
- // `createCamundaClient`, which upgrades the throughput-critical path — process-instance
3
- // creation — to the Falcon protocol (falling back to REST). This adapter lets the Urban
4
- // runtime use that same client, so the runtime and the IDE talk to the engine the same way
5
- // (ADR 0053).
1
+ // The Urban runtime's single engine client. Every path to a Nano engine — deploy,
2
+ // instance creation, message publication, user tasks, and job workers — routes
3
+ // through one `@nanobpm/nano-sdk` client (ADR 0055). The SDK's `createCamundaClient`
4
+ // transparently upgrades the throughput-critical paths (process-instance creation and
5
+ // job serving) to the Falcon protocol on a Nano server and falls back to REST
6
+ // everywhere else, so the runtime and the IDE talk to the engine the same way with a
7
+ // single transport instead of a hand-rolled REST client plus a Falcon shim.
6
8
  //
7
- // Scope: the adapter routes instance creation (the hot path Falcon actually upgrades) through
8
- // the SDK client, and delegates the cold paths (deploy, publish, user tasks, workers) to a
9
- // REST fallback with the same address/token. `@nanobpm/nano-sdk` is an OPTIONAL dependency —
10
- // imported lazily — so the runtime keeps a dependency-free core and REST-only default.
9
+ // `@nanobpm/nano-sdk` is a direct dependency, but it is imported lazily (via an
10
+ // indirected specifier) so a caller that injects its own client — unit tests, the
11
+ // Deno smoke, or an author bringing the embedded transport — never loads it, keeping
12
+ // a dependency-free import graph for those paths.
11
13
 
12
- import type { EngineClient, JobHandler, WorkerSubscription } from "../core/host.ts";
13
- import { RestEngineClient, requireProcessInstanceKey } from "./rest.ts";
14
+ import type {
15
+ EngineClient,
16
+ EngineJob,
17
+ JobHandler,
18
+ WorkerSubscription,
19
+ } from "../core/host.ts";
20
+ import type { EngineSdkClient } from "./sdk.ts";
14
21
 
15
- /** The subset of the `@nanobpm/nano-sdk` client the adapter uses. */
22
+ /** Coerce an engine response's process-instance key to a non-empty string, or
23
+ * throw — a missing key means a malformed/partial response, not a real instance. */
24
+ export function requireProcessInstanceKey(key: string | number | null | undefined): string {
25
+ if (key == null || key === "") {
26
+ throw new Error("engine response missing processInstanceKey/key");
27
+ }
28
+ return String(key);
29
+ }
30
+
31
+ /** A job as delivered to a nano-sdk job handler: the frame fields plus the
32
+ * acknowledgement actions the handler must call. */
33
+ export interface NanoSdkActivatedJob {
34
+ jobKey: string;
35
+ type?: string;
36
+ processInstanceKey?: string | number;
37
+ elementId?: string;
38
+ variables?: Record<string, unknown>;
39
+ complete(variables?: Record<string, unknown>): Promise<unknown>;
40
+ fail(body: { errorMessage: string; retries?: number }): Promise<unknown>;
41
+ }
42
+
43
+ /** Config for a nano-sdk job worker (the subset this adapter sets). */
44
+ export interface NanoSdkJobWorkerConfig {
45
+ jobType: string;
46
+ jobHandler: (job: NanoSdkActivatedJob) => Promise<unknown> | unknown;
47
+ workerName?: string;
48
+ maxParallelJobs?: number;
49
+ jobTimeoutMs?: number;
50
+ pollTimeoutMs?: number;
51
+ fetchVariables?: string[];
52
+ /** Start polling immediately. This adapter sets it false and starts explicitly. */
53
+ autoStart?: boolean;
54
+ }
55
+
56
+ /** The handle returned by `createJobWorker`. */
57
+ export interface NanoSdkJobWorker {
58
+ start(): void | Promise<void>;
59
+ stop(): void | Promise<void>;
60
+ stopGracefully?(opts?: { waitUpToMs?: number }): Promise<void>;
61
+ }
62
+
63
+ /**
64
+ * The subset of the `@nanobpm/nano-sdk` (Camunda orchestration-cluster) client the
65
+ * engine adapter uses. `createCamundaClient` returns a superset of this, so a test —
66
+ * or an author bringing their own transport (e.g. the embedded engine) — can inject
67
+ * any object satisfying it.
68
+ */
16
69
  export interface NanoSdkClient {
70
+ createDeployment(
71
+ input: { resources: File[]; [k: string]: unknown },
72
+ options?: unknown,
73
+ ): Promise<Record<string, unknown>>;
17
74
  createProcessInstance(
18
75
  input: {
19
76
  processDefinitionId: string;
@@ -22,26 +79,214 @@ export interface NanoSdkClient {
22
79
  },
23
80
  options?: unknown,
24
81
  ): Promise<Record<string, unknown>>;
82
+ publishMessage(
83
+ input: { name: string; correlationKey?: string; variables?: Record<string, unknown> },
84
+ options?: unknown,
85
+ ): Promise<Record<string, unknown>>;
86
+ searchUserTasks(
87
+ input: { filter?: Record<string, unknown> },
88
+ consistency?: unknown,
89
+ options?: unknown,
90
+ ): Promise<Record<string, unknown>>;
91
+ completeUserTask(
92
+ input: { userTaskKey: string; variables?: Record<string, unknown> },
93
+ options?: unknown,
94
+ ): Promise<unknown>;
95
+ createJobWorker(cfg: NanoSdkJobWorkerConfig): NanoSdkJobWorker;
25
96
  close?(): void | Promise<void>;
26
97
  }
27
98
 
99
+ type Log = (level: "info" | "warn" | "error", msg: string, fields?: Record<string, unknown>) => void;
100
+
101
+ /**
102
+ * An `EngineClient` backed entirely by a `@nanobpm/nano-sdk` client. Deploy, create,
103
+ * message, and user-task calls map one-to-one onto the SDK's client methods; workers
104
+ * are the SDK's own job worker, which handles activation, completion, failure, and
105
+ * long-poll/backoff reconnect behind the same transport.
106
+ */
107
+ export class SdkEngineClient implements EngineClient {
108
+ private readonly workers = new Set<NanoSdkJobWorker>();
109
+ private readonly client: NanoSdkClient;
110
+ private readonly log: Log;
111
+
112
+ constructor(client: NanoSdkClient, log: Log = () => {}) {
113
+ this.client = client;
114
+ this.log = log;
115
+ }
116
+
117
+ /**
118
+ * The underlying `@nanobpm/nano-sdk` client — the full Camunda orchestration-cluster
119
+ * surface (decisions, cluster variables, incidents, agents, batch operations, …) over
120
+ * the same connection. Surfaced to handlers as `AppApi.sdk`.
121
+ */
122
+ get sdk(): EngineSdkClient {
123
+ return this.client as unknown as EngineSdkClient;
124
+ }
125
+
126
+ async deployResources(
127
+ resources: { name: string; content: string; contentType: string }[],
128
+ ): Promise<{ deployed: number }> {
129
+ const files = resources.map((r) => new File([r.content], r.name, { type: r.contentType }));
130
+ await this.client.createDeployment({ resources: files });
131
+ return { deployed: resources.length };
132
+ }
133
+
134
+ async createInstance(input: {
135
+ processDefinitionId: string;
136
+ variables?: Record<string, unknown>;
137
+ awaitCompletion?: boolean;
138
+ }): Promise<{ processInstanceKey: string; variables?: Record<string, unknown> }> {
139
+ const body = await this.client.createProcessInstance({
140
+ processDefinitionId: input.processDefinitionId,
141
+ variables: input.variables ?? {},
142
+ awaitCompletion: input.awaitCompletion ?? false,
143
+ });
144
+ const key =
145
+ (body.processInstanceKey as string | number | undefined) ??
146
+ (body.key as string | number | undefined);
147
+ return {
148
+ processInstanceKey: requireProcessInstanceKey(key),
149
+ variables: (body.variables as Record<string, unknown> | undefined) ?? undefined,
150
+ };
151
+ }
152
+
153
+ async publishMessage(input: {
154
+ name: string;
155
+ correlationKey?: string;
156
+ variables?: Record<string, unknown>;
157
+ }): Promise<void> {
158
+ await this.client.publishMessage({
159
+ name: input.name,
160
+ correlationKey: input.correlationKey ?? "",
161
+ variables: input.variables ?? {},
162
+ });
163
+ }
164
+
165
+ async searchUserTasks(filter?: {
166
+ processInstanceKey?: string;
167
+ assignee?: string;
168
+ candidateGroup?: string;
169
+ }): Promise<{ userTaskKey: string; elementId?: string; variables?: Record<string, unknown> }[]> {
170
+ // User tasks are an eventually consistent read; ask for zero-wait consistency so
171
+ // the search reflects what is currently visible without blocking.
172
+ const body = await this.client.searchUserTasks(
173
+ { filter: (filter ?? {}) as Record<string, unknown> },
174
+ { consistency: { waitUpToMs: 0 } },
175
+ );
176
+ const items = (body.items as Record<string, unknown>[] | undefined) ?? [];
177
+ return items.flatMap((it) => {
178
+ const userTaskKey = it.userTaskKey ?? it.key;
179
+ if (userTaskKey == null || userTaskKey === "") {
180
+ this.log("warn", "skipping user task with no key in engine response");
181
+ return [];
182
+ }
183
+ return [{
184
+ userTaskKey: String(userTaskKey),
185
+ elementId: it.elementId as string | undefined,
186
+ variables: it.variables as Record<string, unknown> | undefined,
187
+ }];
188
+ });
189
+ }
190
+
191
+ async completeUserTask(userTaskKey: string, variables?: Record<string, unknown>): Promise<void> {
192
+ await this.client.completeUserTask({ userTaskKey, variables: variables ?? {} });
193
+ }
194
+
195
+ async registerWorker(
196
+ jobType: string,
197
+ handler: JobHandler,
198
+ options?: { workerName?: string; maxParallelJobs?: number; fetchVariables?: string[] },
199
+ ): Promise<WorkerSubscription> {
200
+ const worker = this.client.createJobWorker({
201
+ jobType,
202
+ workerName: options?.workerName ?? `urban:${jobType}`,
203
+ maxParallelJobs: options?.maxParallelJobs ?? 8,
204
+ fetchVariables: options?.fetchVariables,
205
+ // Own the poll lifecycle here rather than leaving it to the SDK's auto-start
206
+ // default, so nothing polls until `start()` is called below.
207
+ autoStart: false,
208
+ jobHandler: async (job) => {
209
+ const rawKey = job.jobKey;
210
+ if (rawKey == null || rawKey === "") {
211
+ this.log("warn", `activation ${jobType}: skipping job with no jobKey in engine response`);
212
+ return undefined;
213
+ }
214
+ const engineJob: EngineJob = {
215
+ jobKey: String(rawKey),
216
+ jobType,
217
+ processInstanceKey:
218
+ job.processInstanceKey != null ? String(job.processInstanceKey) : undefined,
219
+ elementId: job.elementId,
220
+ variables: job.variables ?? {},
221
+ };
222
+ try {
223
+ const out = await handler(engineJob);
224
+ return await job.complete((out as Record<string, unknown> | undefined) ?? {});
225
+ } catch (err) {
226
+ const message = err instanceof Error ? err.message : String(err);
227
+ this.log("error", `handler ${jobType} threw`, { err: message });
228
+ try {
229
+ // Best-effort failure report; the engine redelivers after the lock
230
+ // times out (at-least-once → handlers must be idempotent). Await +
231
+ // swallow so a rejected `fail` cannot escape as an unhandled rejection.
232
+ return await job.fail({ errorMessage: message.slice(0, 500), retries: 0 });
233
+ } catch {
234
+ return undefined;
235
+ }
236
+ }
237
+ },
238
+ });
239
+ await worker.start();
240
+ this.workers.add(worker);
241
+
242
+ return {
243
+ jobType,
244
+ unsubscribe: async () => {
245
+ await this.stopWorker(worker);
246
+ this.workers.delete(worker);
247
+ },
248
+ };
249
+ }
250
+
251
+ /** Stop one worker, tolerating a Falcon worker whose transport never bound
252
+ * (detection still pending) — its `stop` throws, which must not fail teardown. */
253
+ private async stopWorker(worker: NanoSdkJobWorker): Promise<void> {
254
+ try {
255
+ await (worker.stopGracefully ? worker.stopGracefully() : worker.stop());
256
+ } catch {
257
+ /* worker never fully started (transport unbound); nothing to drain */
258
+ }
259
+ }
260
+
261
+ async close(): Promise<void> {
262
+ for (const w of this.workers) await this.stopWorker(w);
263
+ this.workers.clear();
264
+ try {
265
+ await this.client.close?.();
266
+ } catch {
267
+ /* transport already closed / never opened */
268
+ }
269
+ }
270
+ }
271
+
28
272
  export interface NanoSdkEngineOptions {
29
273
  /** REST base, e.g. http://localhost:8080/v2. */
30
274
  restAddress: string;
31
275
  token?: string;
32
- /** CAMUNDA_TRANSPORT: "auto" | "falcon" | "rest". Passed to createCamundaClient. */
276
+ /** CAMUNDA_TRANSPORT: "auto" | "falcon" | "rest" | "embedded". Passed to createCamundaClient. */
33
277
  transport?: string;
34
- log?: (level: "info" | "warn" | "error", msg: string, fields?: Record<string, unknown>) => void;
278
+ log?: Log;
279
+ /** Test seam: inject a ready-made SDK client (or a compatible fake). */
280
+ client?: NanoSdkClient;
35
281
  /** Test seam: provide the SDK client factory instead of importing @nanobpm/nano-sdk. */
36
282
  createClient?: (opts: { restAddress: string; token?: string; transport?: string }) => NanoSdkClient;
37
- /** Test seam / customisation: the cold-path client. Defaults to a RestEngineClient. */
38
- fallback?: EngineClient;
39
283
  }
40
284
 
41
285
  async function importNanoSdk(): Promise<
42
286
  (opts: Record<string, unknown>) => NanoSdkClient
43
287
  > {
44
- // Indirect the specifier so the (optional, peer) module is not resolved at typecheck time.
288
+ // Indirect the specifier so the (lazily loaded) module is not resolved at
289
+ // typecheck time and stays out of the import graph for injected-client paths.
45
290
  const spec = "@nanobpm/nano-sdk";
46
291
  const mod = (await import(spec)) as {
47
292
  createCamundaClient?: (opts: Record<string, unknown>) => NanoSdkClient;
@@ -53,71 +298,32 @@ async function importNanoSdk(): Promise<
53
298
  }
54
299
 
55
300
  /**
56
- * Build an EngineClient backed by `@nanobpm/nano-sdk` for instance creation (Falcon) and a REST
57
- * fallback for everything else. Returns `null` if the SDK cannot be loaded (and no test seam was
58
- * given), so callers can fall back to plain REST.
301
+ * Build the Urban runtime's `EngineClient`, backed by a single `@nanobpm/nano-sdk`
302
+ * client. Provide a `client`/`createClient` seam to inject a fake (tests, embedded
303
+ * transport); otherwise a client is constructed from `restAddress`/`token`/`transport`.
59
304
  */
60
305
  export async function createNanoSdkEngineClient(
61
306
  opts: NanoSdkEngineOptions,
62
- ): Promise<EngineClient | null> {
307
+ ): Promise<EngineClient> {
63
308
  const log = opts.log ?? (() => {});
64
309
  let client: NanoSdkClient;
65
- try {
66
- if (opts.createClient) {
67
- client = opts.createClient({
68
- restAddress: opts.restAddress,
69
- token: opts.token,
70
- transport: opts.transport,
71
- });
72
- } else {
73
- const createCamundaClient = await importNanoSdk();
74
- client = createCamundaClient({
75
- config: {
76
- CAMUNDA_REST_ADDRESS: opts.restAddress,
77
- CAMUNDA_TOKEN: opts.token,
78
- CAMUNDA_TRANSPORT: opts.transport ?? "auto",
79
- },
80
- });
81
- }
82
- } catch (err) {
83
- log("warn", "nano-sdk unavailable; using REST transport", { error: String(err) });
84
- return null;
310
+ if (opts.client) {
311
+ client = opts.client;
312
+ } else if (opts.createClient) {
313
+ client = opts.createClient({
314
+ restAddress: opts.restAddress,
315
+ token: opts.token,
316
+ transport: opts.transport,
317
+ });
318
+ } else {
319
+ const createCamundaClient = await importNanoSdk();
320
+ client = createCamundaClient({
321
+ config: {
322
+ CAMUNDA_REST_ADDRESS: opts.restAddress,
323
+ ...(opts.token ? { CAMUNDA_TOKEN: opts.token } : {}),
324
+ CAMUNDA_TRANSPORT: opts.transport ?? "auto",
325
+ },
326
+ });
85
327
  }
86
-
87
- const fallback =
88
- opts.fallback ?? new RestEngineClient({ baseUrl: opts.restAddress, token: opts.token, log });
89
-
90
- const adapter: EngineClient = {
91
- deployResources: (resources) => fallback.deployResources(resources),
92
-
93
- async createInstance(input) {
94
- const body = await client.createProcessInstance({
95
- processDefinitionId: input.processDefinitionId,
96
- variables: input.variables,
97
- awaitCompletion: input.awaitCompletion ?? false,
98
- });
99
- const key =
100
- (body.processInstanceKey as string | number | undefined) ?? (body.key as string | number | undefined);
101
- return {
102
- processInstanceKey: requireProcessInstanceKey(key),
103
- variables: (body.variables as Record<string, unknown> | undefined) ?? undefined,
104
- };
105
- },
106
-
107
- publishMessage: (input) => fallback.publishMessage(input),
108
- searchUserTasks: (filter) => fallback.searchUserTasks(filter),
109
- completeUserTask: (key, vars) => fallback.completeUserTask(key, vars),
110
-
111
- registerWorker: (jobType: string, handler: JobHandler, options): Promise<WorkerSubscription> =>
112
- fallback.registerWorker(jobType, handler, options),
113
-
114
- async close() {
115
- try {
116
- await client.close?.();
117
- } finally {
118
- await fallback.close();
119
- }
120
- },
121
- };
122
- return adapter;
328
+ return new SdkEngineClient(client, log);
123
329
  }
@@ -0,0 +1,18 @@
1
+ // The full nano-sdk engine client, surfaced to code-first Urban authors.
2
+ //
3
+ // `AppApi.engine` is the transport-agnostic seam (deploy, create, message, user
4
+ // tasks, workers). When the app runs on the nano-sdk transport, `AppApi.sdk` also
5
+ // carries the underlying `@nanobpm/nano-sdk` client, giving handlers the whole
6
+ // Camunda orchestration-cluster surface — decisions, cluster variables, incidents,
7
+ // agents, batch operations, and more — over the same connection, without a separate
8
+ // `@nanobpm/nano-sdk` dependency.
9
+ //
10
+ // This module is type-only: the `import(...)` type query resolves nano-sdk's types at
11
+ // typecheck and is fully erased at runtime, so re-exporting it from the runtime barrel
12
+ // adds no load-time dependency on nano-sdk.
13
+
14
+ /** The full nano-sdk (Camunda orchestration-cluster) engine client type — the value
15
+ * returned by `createCamundaClient`. */
16
+ export type EngineSdkClient = ReturnType<
17
+ typeof import("@nanobpm/nano-sdk").createCamundaClient
18
+ >;
@@ -60,9 +60,13 @@ export { evalCorrelation } from "./core/modules/triggers.ts";
60
60
  export { createNodeHost } from "./adapters/node.ts";
61
61
  export { createDenoHost } from "./adapters/deno.ts";
62
62
  export { isDeno, selectHost } from "./adapters/detect.ts";
63
- export { RestEngineClient } from "./engine/rest.ts";
64
- export type { RestEngineOptions } from "./engine/rest.ts";
65
- export { createNanoSdkEngineClient } from "./engine/nanosdk.ts";
66
- export type { NanoSdkEngineOptions, NanoSdkClient } from "./engine/nanosdk.ts";
63
+ export { createNanoSdkEngineClient, SdkEngineClient } from "./engine/nanosdk.ts";
64
+ export type {
65
+ NanoSdkClient,
66
+ NanoSdkEngineOptions,
67
+ NanoSdkJobWorker,
68
+ NanoSdkJobWorkerConfig,
69
+ } from "./engine/nanosdk.ts";
70
+ export type { EngineSdkClient } from "./engine/sdk.ts";
67
71
  export { runFromEnv } from "./run.ts";
68
72
  export type { RunOptions } from "./run.ts";
@@ -1,10 +1,9 @@
1
1
  // runFromEnv — the one-call entrypoint a scaffolded app's main and the `urban run` CLI both
2
- // use: select the host for the current runtime, build a REST engine client from env, create
3
- // the Urban app, start it, and install a graceful-shutdown handler.
2
+ // use: select the host for the current runtime, build the nano-sdk engine client from env,
3
+ // create the Urban app, start it, and install a graceful-shutdown handler.
4
4
 
5
5
  import { createUrbanApp, type CreateUrbanAppOptions, type UrbanApp } from "./core/runtime.ts";
6
6
  import { selectHost } from "./adapters/detect.ts";
7
- import { RestEngineClient } from "./engine/rest.ts";
8
7
  import { createNanoSdkEngineClient } from "./engine/nanosdk.ts";
9
8
  import type { EngineClient } from "./core/host.ts";
10
9
 
@@ -14,9 +13,10 @@ export interface RunOptions extends Partial<CreateUrbanAppOptions> {
14
13
  /** Engine REST base. Default $CAMUNDA_REST_ADDRESS or http://localhost:8080/v2. */
15
14
  restAddress?: string;
16
15
  /**
17
- * Engine transport: "rest" (default) uses the built-in REST client; "auto" | "falcon" try
18
- * `@nanobpm/nano-sdk` (Falcon on instance creation, REST fallback), falling back to REST if the
19
- * SDK is not installed. Overridable via $CAMUNDA_TRANSPORT.
16
+ * Engine transport passed to `@nanobpm/nano-sdk`: "auto" (default) upgrades
17
+ * process-instance creation and job serving to the Falcon protocol on a Nano
18
+ * server and falls back to REST elsewhere; "falcon" forces it; "rest" never
19
+ * upgrades; "embedded" runs an in-process engine. Overridable via $CAMUNDA_TRANSPORT.
20
20
  */
21
21
  transport?: string;
22
22
  /** Install SIGINT/SIGTERM handlers to stop the app. Default true. */
@@ -27,15 +27,16 @@ export async function runFromEnv(opts: RunOptions = {}): Promise<UrbanApp> {
27
27
  const host = opts.host ?? selectHost({ cwd: opts.root });
28
28
  const restAddress =
29
29
  opts.restAddress ?? host.env("CAMUNDA_REST_ADDRESS") ?? "http://localhost:8080/v2";
30
- const transport = opts.transport ?? host.env("CAMUNDA_TRANSPORT") ?? "rest";
30
+ const transport = opts.transport ?? host.env("CAMUNDA_TRANSPORT") ?? "auto";
31
31
 
32
- let engine: EngineClient | undefined = opts.engine;
33
- if (!engine && transport !== "rest") {
34
- engine =
35
- (await createNanoSdkEngineClient({ restAddress, token: host.env("CAMUNDA_TOKEN"), transport, log: host.log })) ??
36
- undefined;
37
- }
38
- engine ??= new RestEngineClient({ baseUrl: restAddress, token: host.env("CAMUNDA_TOKEN"), log: host.log });
32
+ const engine: EngineClient =
33
+ opts.engine ??
34
+ (await createNanoSdkEngineClient({
35
+ restAddress,
36
+ token: host.env("CAMUNDA_TOKEN"),
37
+ transport,
38
+ log: host.log,
39
+ }));
39
40
 
40
41
  const app = await createUrbanApp({
41
42
  host,