@nanobpm/workflow 0.13.0 → 0.13.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.
package/dist/client.d.ts CHANGED
@@ -66,6 +66,37 @@ export interface NanoJobWorker {
66
66
  waitUpToMs?: number;
67
67
  }): Promise<void>;
68
68
  }
69
+ /** The subset of a raw `@nanobpm/nano-sdk` job worker the adapter drives. Its
70
+ * `start()` may be synchronous or async: on nano-sdk >=1.2.5's Falcon/auto path
71
+ * it is async and the SDK self-starts the worker after an asynchronous transport
72
+ * bind (see {@link adaptJobWorker}). */
73
+ export interface RawNanoJobWorker {
74
+ start(): void | Promise<void>;
75
+ stop(): void | Promise<void>;
76
+ stopGracefully(opts?: {
77
+ waitUpToMs?: number;
78
+ }): Promise<unknown>;
79
+ }
80
+ /** Adapt a raw `@nanobpm/nano-sdk` job worker to the {@link NanoJobWorker} handle
81
+ * the Worker runtime drives, making the eager `start()` NULL-SAFE.
82
+ *
83
+ * nano-sdk >=1.2.5's Falcon/auto worker SELF-STARTS after an ASYNCHRONOUS
84
+ * transport bind: `createJobWorker` hands back a worker whose transport is null
85
+ * until Nano is detected, at which point the SDK `bindTransport(t)`s it and
86
+ * `start()`s the worker ITSELF. Calling `start()` before that bind dereferences
87
+ * the null transport and — because `start()` is async — surfaces as an UNHANDLED
88
+ * REJECTION that crashes the process
89
+ * (`TypeError: Cannot read properties of null (reading 'subscribe')`, #415). The
90
+ * REST/manual worker, by contrast, never self-starts, so `start()` must still be
91
+ * called here. We resolve the version-skew by starting eagerly but NULL-SAFELY:
92
+ * swallow ONLY the pre-bind race (the SDK self-starts once the transport binds)
93
+ * and SURFACE any other start failure via `console.warn` rather than masking it.
94
+ * The SDK owns the start lifecycle end to end (it logs and falls back to REST on
95
+ * its own subscribe failure), so discarding the resolved result — as the previous
96
+ * `void worker.start()` already did — is safe. Pairs with the library-side
97
+ * null-safe, idempotent start (jwulf/nano-sdk-js#12) so neither an eager nor a
98
+ * duplicate start can crash. */
99
+ export declare function adaptJobWorker(worker: RawNanoJobWorker): NanoJobWorker;
69
100
  /** An activated job as delivered to a nano-sdk job handler: the workflow `Job`
70
101
  * fields plus the acknowledgement actions. */
71
102
  export type ActivatedJob = Job & {
package/dist/client.js CHANGED
@@ -96,6 +96,71 @@ export async function toDeployableBpmn(wf, opts = {}) {
96
96
  return semantic;
97
97
  }
98
98
  }
99
+ /** Adapt a raw `@nanobpm/nano-sdk` job worker to the {@link NanoJobWorker} handle
100
+ * the Worker runtime drives, making the eager `start()` NULL-SAFE.
101
+ *
102
+ * nano-sdk >=1.2.5's Falcon/auto worker SELF-STARTS after an ASYNCHRONOUS
103
+ * transport bind: `createJobWorker` hands back a worker whose transport is null
104
+ * until Nano is detected, at which point the SDK `bindTransport(t)`s it and
105
+ * `start()`s the worker ITSELF. Calling `start()` before that bind dereferences
106
+ * the null transport and — because `start()` is async — surfaces as an UNHANDLED
107
+ * REJECTION that crashes the process
108
+ * (`TypeError: Cannot read properties of null (reading 'subscribe')`, #415). The
109
+ * REST/manual worker, by contrast, never self-starts, so `start()` must still be
110
+ * called here. We resolve the version-skew by starting eagerly but NULL-SAFELY:
111
+ * swallow ONLY the pre-bind race (the SDK self-starts once the transport binds)
112
+ * and SURFACE any other start failure via `console.warn` rather than masking it.
113
+ * The SDK owns the start lifecycle end to end (it logs and falls back to REST on
114
+ * its own subscribe failure), so discarding the resolved result — as the previous
115
+ * `void worker.start()` already did — is safe. Pairs with the library-side
116
+ * null-safe, idempotent start (jwulf/nano-sdk-js#12) so neither an eager nor a
117
+ * duplicate start can crash. */
118
+ export function adaptJobWorker(worker) {
119
+ return {
120
+ start: () => {
121
+ try {
122
+ const started = worker.start();
123
+ if (isPromiseLike(started))
124
+ started.then(undefined, handleStartError);
125
+ }
126
+ catch (e) {
127
+ handleStartError(e);
128
+ }
129
+ },
130
+ stop: () => worker.stop(),
131
+ stopGracefully: async (opts) => {
132
+ await worker.stopGracefully(opts);
133
+ },
134
+ };
135
+ }
136
+ function isPromiseLike(v) {
137
+ return isRecord(v) && typeof v.then === "function";
138
+ }
139
+ /** The SDK's pre-bind start race is a NULL/undefined-transport dereference the SDK
140
+ * recovers from via its own self-start: a `TypeError` from reading the transport's
141
+ * `subscribe` off a `null`/`undefined` transport
142
+ * (`Cannot read properties of null (reading 'subscribe')`, #415). Match it NARROWLY
143
+ * — the specific `subscribe` dereference signature, not any null/undefined deref —
144
+ * so a genuine start failure on the REST/manual path (or an unrelated null-deref
145
+ * bug during `start()`) is never mistaken for the race and silently masked. */
146
+ function isPreBindStartRace(e) {
147
+ return (e instanceof TypeError &&
148
+ /Cannot read propert(?:y|ies) of (?:null|undefined) \(reading ['"]subscribe['"]\)/.test(e.message));
149
+ }
150
+ /** Handle an eager-start error NULL-SAFELY. Swallow ONLY the SDK's known pre-bind
151
+ * transport race (it self-starts once its transport binds — see
152
+ * {@link adaptJobWorker}); any OTHER error is a real start failure (e.g. on the
153
+ * REST/manual path) and is SURFACED via `console.warn` so it is not silently
154
+ * masked. Never rethrows — callers rely on this to keep an async `start()` from
155
+ * escaping as an unhandled rejection that crashes the process. The SDK owns the
156
+ * start lifecycle end to end (it logs and falls back to REST on its own subscribe
157
+ * failure), so we surface-and-continue rather than propagate. */
158
+ function handleStartError(e) {
159
+ if (isPreBindStartRace(e))
160
+ return;
161
+ console.warn("[@nanobpm/workflow] job worker start() failed (not the known pre-bind transport " +
162
+ "race); the SDK owns start and REST fallback, so this is surfaced but not rethrown:", e);
163
+ }
99
164
  export class WorkflowError extends Error {
100
165
  status;
101
166
  body;
@@ -153,15 +218,7 @@ export class WorkflowClient {
153
218
  return JobActionReceiptSymbol;
154
219
  },
155
220
  });
156
- return {
157
- start: () => {
158
- void worker.start();
159
- },
160
- stop: () => worker.stop(),
161
- stopGracefully: async (opts) => {
162
- await worker.stopGracefully(opts);
163
- },
164
- };
221
+ return adaptJobWorker(worker);
165
222
  },
166
223
  };
167
224
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/workflow",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
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",
package/src/client.ts CHANGED
@@ -154,6 +154,87 @@ export interface NanoJobWorker {
154
154
  stopGracefully?(opts?: { waitUpToMs?: number }): Promise<void>;
155
155
  }
156
156
 
157
+ /** The subset of a raw `@nanobpm/nano-sdk` job worker the adapter drives. Its
158
+ * `start()` may be synchronous or async: on nano-sdk >=1.2.5's Falcon/auto path
159
+ * it is async and the SDK self-starts the worker after an asynchronous transport
160
+ * bind (see {@link adaptJobWorker}). */
161
+ export interface RawNanoJobWorker {
162
+ start(): void | Promise<void>;
163
+ stop(): void | Promise<void>;
164
+ stopGracefully(opts?: { waitUpToMs?: number }): Promise<unknown>;
165
+ }
166
+
167
+ /** Adapt a raw `@nanobpm/nano-sdk` job worker to the {@link NanoJobWorker} handle
168
+ * the Worker runtime drives, making the eager `start()` NULL-SAFE.
169
+ *
170
+ * nano-sdk >=1.2.5's Falcon/auto worker SELF-STARTS after an ASYNCHRONOUS
171
+ * transport bind: `createJobWorker` hands back a worker whose transport is null
172
+ * until Nano is detected, at which point the SDK `bindTransport(t)`s it and
173
+ * `start()`s the worker ITSELF. Calling `start()` before that bind dereferences
174
+ * the null transport and — because `start()` is async — surfaces as an UNHANDLED
175
+ * REJECTION that crashes the process
176
+ * (`TypeError: Cannot read properties of null (reading 'subscribe')`, #415). The
177
+ * REST/manual worker, by contrast, never self-starts, so `start()` must still be
178
+ * called here. We resolve the version-skew by starting eagerly but NULL-SAFELY:
179
+ * swallow ONLY the pre-bind race (the SDK self-starts once the transport binds)
180
+ * and SURFACE any other start failure via `console.warn` rather than masking it.
181
+ * The SDK owns the start lifecycle end to end (it logs and falls back to REST on
182
+ * its own subscribe failure), so discarding the resolved result — as the previous
183
+ * `void worker.start()` already did — is safe. Pairs with the library-side
184
+ * null-safe, idempotent start (jwulf/nano-sdk-js#12) so neither an eager nor a
185
+ * duplicate start can crash. */
186
+ export function adaptJobWorker(worker: RawNanoJobWorker): NanoJobWorker {
187
+ return {
188
+ start: () => {
189
+ try {
190
+ const started = worker.start();
191
+ if (isPromiseLike(started)) started.then(undefined, handleStartError);
192
+ } catch (e) {
193
+ handleStartError(e);
194
+ }
195
+ },
196
+ stop: () => worker.stop(),
197
+ stopGracefully: async (opts) => {
198
+ await worker.stopGracefully(opts);
199
+ },
200
+ };
201
+ }
202
+
203
+ function isPromiseLike(v: unknown): v is PromiseLike<unknown> {
204
+ return isRecord(v) && typeof v.then === "function";
205
+ }
206
+
207
+ /** The SDK's pre-bind start race is a NULL/undefined-transport dereference the SDK
208
+ * recovers from via its own self-start: a `TypeError` from reading the transport's
209
+ * `subscribe` off a `null`/`undefined` transport
210
+ * (`Cannot read properties of null (reading 'subscribe')`, #415). Match it NARROWLY
211
+ * — the specific `subscribe` dereference signature, not any null/undefined deref —
212
+ * so a genuine start failure on the REST/manual path (or an unrelated null-deref
213
+ * bug during `start()`) is never mistaken for the race and silently masked. */
214
+ function isPreBindStartRace(e: unknown): boolean {
215
+ return (
216
+ e instanceof TypeError &&
217
+ /Cannot read propert(?:y|ies) of (?:null|undefined) \(reading ['"]subscribe['"]\)/.test(e.message)
218
+ );
219
+ }
220
+
221
+ /** Handle an eager-start error NULL-SAFELY. Swallow ONLY the SDK's known pre-bind
222
+ * transport race (it self-starts once its transport binds — see
223
+ * {@link adaptJobWorker}); any OTHER error is a real start failure (e.g. on the
224
+ * REST/manual path) and is SURFACED via `console.warn` so it is not silently
225
+ * masked. Never rethrows — callers rely on this to keep an async `start()` from
226
+ * escaping as an unhandled rejection that crashes the process. The SDK owns the
227
+ * start lifecycle end to end (it logs and falls back to REST on its own subscribe
228
+ * failure), so we surface-and-continue rather than propagate. */
229
+ function handleStartError(e: unknown): void {
230
+ if (isPreBindStartRace(e)) return;
231
+ console.warn(
232
+ "[@nanobpm/workflow] job worker start() failed (not the known pre-bind transport " +
233
+ "race); the SDK owns start and REST fallback, so this is surfaced but not rethrown:",
234
+ e,
235
+ );
236
+ }
237
+
157
238
  /** An activated job as delivered to a nano-sdk job handler: the workflow `Job`
158
239
  * fields plus the acknowledgement actions. */
159
240
  export type ActivatedJob = Job & {
@@ -253,15 +334,7 @@ export class WorkflowClient {
253
334
  return JobActionReceiptSymbol;
254
335
  },
255
336
  });
256
- return {
257
- start: () => {
258
- void worker.start();
259
- },
260
- stop: () => worker.stop(),
261
- stopGracefully: async (opts) => {
262
- await worker.stopGracefully(opts);
263
- },
264
- };
337
+ return adaptJobWorker(worker);
265
338
  },
266
339
  };
267
340
  }