@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}
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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}
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 {
|