@nanobpm/urban 0.11.0 → 0.12.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/cli.js CHANGED
@@ -27,7 +27,7 @@ function readVersion() {
27
27
  }
28
28
  const VERSION = readVersion();
29
29
  function parse(argv) {
30
- const f = { root: ".", manifest: "nano.app.json", check: false, help: false, version: false, _: [] };
30
+ const f = { root: ".", manifest: "nano.app.json", check: false, deno: false, help: false, version: false, _: [] };
31
31
  const need = (i, flag) => {
32
32
  const v = argv[i];
33
33
  if (v === undefined || v.startsWith("-"))
@@ -42,6 +42,10 @@ function parse(argv) {
42
42
  f.version = true;
43
43
  else if (a === "--check")
44
44
  f.check = true;
45
+ else if (a === "--deno")
46
+ f.deno = true;
47
+ else if (a === "--")
48
+ continue; // tolerate a bare "--" some runners inject (not an option terminator here)
45
49
  else if (a === "--root")
46
50
  f.root = need(++i, a);
47
51
  else if (a === "--manifest")
@@ -64,7 +68,7 @@ function parse(argv) {
64
68
  const USAGE = `urban — build and run Urban apps (nano.app.json)
65
69
 
66
70
  Usage:
67
- urban new <name> [--root <path>] scaffold a new Urban app
71
+ urban new <name> [--root <path>] [--deno] scaffold a new Urban app
68
72
  urban check validate the manifest
69
73
  urban gen [--check] derive artifacts (migrations, worker-io)
70
74
  urban run materialize + serve the app
@@ -138,13 +142,16 @@ async function cmdDeploy(f) {
138
142
  async function cmdNew(f) {
139
143
  const name = f._[1];
140
144
  if (!name) {
141
- console.error("usage: urban new <name> [--root <path>]");
145
+ console.error("usage: urban new <name> [--root <path>] [--deno]");
142
146
  return 1;
143
147
  }
144
148
  const dir = f.root !== "." ? f.root : `./${slugify(name)}`;
145
- const res = await scaffold({ name, dir });
149
+ const res = await scaffold({ name, dir, deno: f.deno });
146
150
  console.log(`✔ scaffolded "${res.id}" in ${res.dir} (${res.files.length} files)`);
147
- console.log(` cd ${res.dir} && (npm install && npm start) || deno task start`);
151
+ console.log(` cd ${res.dir}`);
152
+ console.log(` npm install && npm start`);
153
+ if (f.deno)
154
+ console.log(` # or on Deno: deno task start`);
148
155
  return 0;
149
156
  }
150
157
  export async function main(argv) {
@@ -26,7 +26,8 @@ export interface NanoSdkJobWorkerConfig {
26
26
  jobTimeoutMs?: number;
27
27
  pollTimeoutMs?: number;
28
28
  fetchVariables?: string[];
29
- /** Start polling immediately. This adapter sets it false and starts explicitly. */
29
+ /** Start polling immediately (the SDK default). The adapter leaves this unset so the
30
+ * SDK owns the start lifecycle after its async Nano/Falcon transport detection. */
30
31
  autoStart?: boolean;
31
32
  }
32
33
  /** The handle returned by `createJobWorker`. */
@@ -69,6 +70,10 @@ export interface NanoSdkClient {
69
70
  variables?: Record<string, unknown>;
70
71
  }, options?: unknown): Promise<unknown>;
71
72
  createJobWorker(cfg: NanoSdkJobWorkerConfig): NanoSdkJobWorker;
73
+ /** Stop every worker created on this client. Used on teardown to also drain a
74
+ * REST-fallback worker, whose handle the `createJobWorker` proxy starts internally
75
+ * and does not hand back. Optional: not every injected client implements it. */
76
+ stopAllWorkers?(): void | Promise<void>;
72
77
  close?(): void | Promise<void>;
73
78
  }
74
79
  type Log = (level: "info" | "warn" | "error", msg: string, fields?: Record<string, unknown>) => void;
@@ -103,9 +103,6 @@ export class SdkEngineClient {
103
103
  workerName: options?.workerName ?? `urban:${jobType}`,
104
104
  maxParallelJobs: options?.maxParallelJobs ?? 8,
105
105
  fetchVariables: options?.fetchVariables,
106
- // Own the poll lifecycle here rather than leaving it to the SDK's auto-start
107
- // default, so nothing polls until `start()` is called below.
108
- autoStart: false,
109
106
  jobHandler: async (job) => {
110
107
  const rawKey = job.jobKey;
111
108
  if (rawKey == null || rawKey === "") {
@@ -138,7 +135,11 @@ export class SdkEngineClient {
138
135
  }
139
136
  },
140
137
  });
141
- await worker.start();
138
+ // The nano-sdk job worker owns its own start lifecycle: `createJobWorker` runs
139
+ // async Nano/Falcon detection and, once the transport is bound, starts polling
140
+ // itself (Falcon) or hands off to an auto-started REST worker. Calling `start()`
141
+ // here would race that detection and dereference a still-null transport, so we
142
+ // don't — the worker autostarts.
142
143
  this.workers.add(worker);
143
144
  return {
144
145
  jobType,
@@ -162,6 +163,14 @@ export class SdkEngineClient {
162
163
  for (const w of this.workers)
163
164
  await this.stopWorker(w);
164
165
  this.workers.clear();
166
+ try {
167
+ // Also drain any REST-fallback worker the SDK started internally (its handle is
168
+ // not the one we hold), which `stopWorker` above cannot reach.
169
+ await this.client.stopAllWorkers?.();
170
+ }
171
+ catch {
172
+ /* no workers running / already stopped */
173
+ }
165
174
  try {
166
175
  await this.client.close?.();
167
176
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -61,7 +61,7 @@
61
61
  "@nanobpm/nano-sdk": "^1.2.0",
62
62
  "@nanobpm/workflow": "^0.7.0",
63
63
  "bpmn-auto-layout": "^2.0.0-alpha.2",
64
- "create-urban-app": "^0.2.0"
64
+ "create-urban-app": "^0.3.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^22.20.1",
package/src/cli.ts CHANGED
@@ -40,13 +40,14 @@ interface Flags {
40
40
  manifest: string;
41
41
  port?: number;
42
42
  check: boolean;
43
+ deno: boolean;
43
44
  help: boolean;
44
45
  version: boolean;
45
46
  _: string[];
46
47
  }
47
48
 
48
49
  function parse(argv: string[]): Flags {
49
- const f: Flags = { root: ".", manifest: "nano.app.json", check: false, help: false, version: false, _: [] };
50
+ const f: Flags = { root: ".", manifest: "nano.app.json", check: false, deno: false, help: false, version: false, _: [] };
50
51
  const need = (i: number, flag: string): string => {
51
52
  const v = argv[i];
52
53
  if (v === undefined || v.startsWith("-")) throw new Error(`flag ${flag} requires a value`);
@@ -57,6 +58,8 @@ function parse(argv: string[]): Flags {
57
58
  if (a === "-h" || a === "--help") f.help = true;
58
59
  else if (a === "-v" || a === "--version") f.version = true;
59
60
  else if (a === "--check") f.check = true;
61
+ else if (a === "--deno") f.deno = true;
62
+ else if (a === "--") continue; // tolerate a bare "--" some runners inject (not an option terminator here)
60
63
  else if (a === "--root") f.root = need(++i, a);
61
64
  else if (a === "--manifest") f.manifest = need(++i, a);
62
65
  else if (a === "--port") {
@@ -75,7 +78,7 @@ function parse(argv: string[]): Flags {
75
78
  const USAGE = `urban — build and run Urban apps (nano.app.json)
76
79
 
77
80
  Usage:
78
- urban new <name> [--root <path>] scaffold a new Urban app
81
+ urban new <name> [--root <path>] [--deno] scaffold a new Urban app
79
82
  urban check validate the manifest
80
83
  urban gen [--check] derive artifacts (migrations, worker-io)
81
84
  urban run materialize + serve the app
@@ -151,13 +154,15 @@ async function cmdDeploy(f: Flags): Promise<number> {
151
154
  async function cmdNew(f: Flags): Promise<number> {
152
155
  const name = f._[1];
153
156
  if (!name) {
154
- console.error("usage: urban new <name> [--root <path>]");
157
+ console.error("usage: urban new <name> [--root <path>] [--deno]");
155
158
  return 1;
156
159
  }
157
160
  const dir = f.root !== "." ? f.root : `./${slugify(name)}`;
158
- const res = await scaffold({ name, dir });
161
+ const res = await scaffold({ name, dir, deno: f.deno });
159
162
  console.log(`✔ scaffolded "${res.id}" in ${res.dir} (${res.files.length} files)`);
160
- console.log(` cd ${res.dir} && (npm install && npm start) || deno task start`);
163
+ console.log(` cd ${res.dir}`);
164
+ console.log(` npm install && npm start`);
165
+ if (f.deno) console.log(` # or on Deno: deno task start`);
161
166
  return 0;
162
167
  }
163
168
 
@@ -81,6 +81,9 @@ function fakeSdkClient(overrides: Partial<NanoSdkClient> = {}): NanoSdkClient &
81
81
  rec.stopped++;
82
82
  },
83
83
  };
84
+ // The real nano-sdk starts the worker itself after async transport detection
85
+ // (autoStart defaults on). Model that here so the adapter must NOT call start().
86
+ if (cfg.autoStart !== false) worker.start();
84
87
  return worker;
85
88
  },
86
89
  close() {
@@ -198,7 +201,7 @@ test("completeUserTask routes through the SDK", async () => {
198
201
  assert.deepEqual(seen, { userTaskKey: "utk", variables: { done: true } });
199
202
  });
200
203
 
201
- test("registerWorker creates a worker (autoStart false), starts it, and dispatches + completes", async () => {
204
+ test("registerWorker creates a worker the SDK auto-starts, and dispatches + completes", async () => {
202
205
  const client = fakeSdkClient();
203
206
  const engine = new SdkEngineClient(client);
204
207
  const handled: Record<string, unknown>[] = [];
@@ -212,7 +215,9 @@ test("registerWorker creates a worker (autoStart false), starts it, and dispatch
212
215
  );
213
216
  assert.equal(sub.jobType, "svc");
214
217
  const rec = client.workers[0];
215
- assert.equal(rec.cfg.autoStart, false);
218
+ // The adapter must not force autoStart off or call start() itself (that races the
219
+ // SDK's async transport detection); the SDK owns the start lifecycle.
220
+ assert.equal(rec.cfg.autoStart, undefined);
216
221
  assert.equal(rec.cfg.workerName, "urban:svc");
217
222
  assert.equal(rec.cfg.maxParallelJobs, 4);
218
223
  assert.equal(rec.started, 1);
@@ -272,6 +277,31 @@ test("close stops every worker and closes the SDK client", async () => {
272
277
  assert.equal(client.closed, 1);
273
278
  });
274
279
 
280
+ test("close drains the REST-fallback worker via the SDK's stopAllWorkers", async () => {
281
+ let stopAllCalls = 0;
282
+ const client = fakeSdkClient({
283
+ async stopAllWorkers() {
284
+ stopAllCalls++;
285
+ },
286
+ });
287
+ const engine = new SdkEngineClient(client);
288
+ await engine.registerWorker("a", async () => ({}));
289
+ await engine.close();
290
+ // The SDK may start a REST-fallback worker whose handle we never receive;
291
+ // close() must reach it through the client's stopAllWorkers().
292
+ assert.equal(stopAllCalls, 1, "close awaits client.stopAllWorkers exactly once");
293
+ assert.equal(client.closed, 1);
294
+ });
295
+
296
+ test("close tolerates a client without stopAllWorkers", async () => {
297
+ const client = fakeSdkClient();
298
+ delete (client as { stopAllWorkers?: unknown }).stopAllWorkers;
299
+ const engine = new SdkEngineClient(client);
300
+ await engine.registerWorker("a", async () => ({}));
301
+ await engine.close();
302
+ assert.equal(client.closed, 1);
303
+ });
304
+
275
305
  test("exposes the underlying nano-sdk client as .sdk", () => {
276
306
  const client = fakeSdkClient();
277
307
  const engine = new SdkEngineClient(client);
@@ -49,7 +49,8 @@ export interface NanoSdkJobWorkerConfig {
49
49
  jobTimeoutMs?: number;
50
50
  pollTimeoutMs?: number;
51
51
  fetchVariables?: string[];
52
- /** Start polling immediately. This adapter sets it false and starts explicitly. */
52
+ /** Start polling immediately (the SDK default). The adapter leaves this unset so the
53
+ * SDK owns the start lifecycle after its async Nano/Falcon transport detection. */
53
54
  autoStart?: boolean;
54
55
  }
55
56
 
@@ -97,6 +98,10 @@ export interface NanoSdkClient {
97
98
  options?: unknown,
98
99
  ): Promise<unknown>;
99
100
  createJobWorker(cfg: NanoSdkJobWorkerConfig): NanoSdkJobWorker;
101
+ /** Stop every worker created on this client. Used on teardown to also drain a
102
+ * REST-fallback worker, whose handle the `createJobWorker` proxy starts internally
103
+ * and does not hand back. Optional: not every injected client implements it. */
104
+ stopAllWorkers?(): void | Promise<void>;
100
105
  close?(): void | Promise<void>;
101
106
  }
102
107
 
@@ -210,9 +215,6 @@ export class SdkEngineClient implements EngineClient {
210
215
  workerName: options?.workerName ?? `urban:${jobType}`,
211
216
  maxParallelJobs: options?.maxParallelJobs ?? 8,
212
217
  fetchVariables: options?.fetchVariables,
213
- // Own the poll lifecycle here rather than leaving it to the SDK's auto-start
214
- // default, so nothing polls until `start()` is called below.
215
- autoStart: false,
216
218
  jobHandler: async (job) => {
217
219
  const rawKey = job.jobKey;
218
220
  if (rawKey == null || rawKey === "") {
@@ -244,7 +246,11 @@ export class SdkEngineClient implements EngineClient {
244
246
  }
245
247
  },
246
248
  });
247
- await worker.start();
249
+ // The nano-sdk job worker owns its own start lifecycle: `createJobWorker` runs
250
+ // async Nano/Falcon detection and, once the transport is bound, starts polling
251
+ // itself (Falcon) or hands off to an auto-started REST worker. Calling `start()`
252
+ // here would race that detection and dereference a still-null transport, so we
253
+ // don't — the worker autostarts.
248
254
  this.workers.add(worker);
249
255
 
250
256
  return {
@@ -269,6 +275,13 @@ export class SdkEngineClient implements EngineClient {
269
275
  async close(): Promise<void> {
270
276
  for (const w of this.workers) await this.stopWorker(w);
271
277
  this.workers.clear();
278
+ try {
279
+ // Also drain any REST-fallback worker the SDK started internally (its handle is
280
+ // not the one we hold), which `stopWorker` above cannot reach.
281
+ await this.client.stopAllWorkers?.();
282
+ } catch {
283
+ /* no workers running / already stopped */
284
+ }
272
285
  try {
273
286
  await this.client.close?.();
274
287
  } catch {