@nanobpm/urban 0.11.0 → 0.11.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.
@@ -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.11.1",
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",
@@ -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 {