c8ctl-plugin-nano 1.52.0 → 1.53.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/agentic-endpoint.mjs +27 -4
- package/c8ctl-plugin.js +168 -274
- package/package.json +10 -10
- package/supervisor-engine.mjs +150 -42
- package/supervisor.dist.js +2 -2
- package/work-relay.mjs +147 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.53.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
|
|
6
6
|
"main": "c8ctl-plugin.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"build:supervisor": "node supervisor/build.mjs",
|
|
40
40
|
"vendor:effect": "node scripts/vendor-effect.mjs",
|
|
41
41
|
"test:supervisor": "node --experimental-strip-types --test supervisor/test/*.test.ts",
|
|
42
|
-
"test": "node --check c8ctl-plugin.js && npm run typecheck:supervisor && npm run build:supervisor && npm run test:supervisor && node --test",
|
|
42
|
+
"test": "node --check c8ctl-plugin.js && npm run typecheck:supervisor && npm run build:supervisor && npm run test:supervisor && node --test --test-timeout=120000 --test-force-exit",
|
|
43
43
|
"prepublishOnly": "npm run typecheck:supervisor && npm run build:supervisor"
|
|
44
44
|
},
|
|
45
45
|
"license": "MIT",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@semantic-release/github": "^12.0.6",
|
|
61
61
|
"@types/node": "^22.20.1",
|
|
62
62
|
"effect": "4.0.0-rc.112",
|
|
63
|
-
"esbuild": "^0.
|
|
63
|
+
"esbuild": "^0.28.0",
|
|
64
64
|
"semantic-release": "^25.0.3",
|
|
65
65
|
"typescript": "^5.9.3"
|
|
66
66
|
},
|
|
@@ -70,12 +70,12 @@
|
|
|
70
70
|
},
|
|
71
71
|
"optionalDependencies": {
|
|
72
72
|
"node-pty": "^1.0.0",
|
|
73
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.
|
|
74
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
75
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
76
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
77
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
78
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
79
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.
|
|
73
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.53.1",
|
|
74
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.53.1",
|
|
75
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.53.1",
|
|
76
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.53.1",
|
|
77
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.53.1",
|
|
78
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.53.1",
|
|
79
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.53.1"
|
|
80
80
|
}
|
|
81
81
|
}
|
package/supervisor-engine.mjs
CHANGED
|
@@ -4,19 +4,45 @@
|
|
|
4
4
|
* (issue #156).
|
|
5
5
|
*
|
|
6
6
|
* #154's activation loop deliberately does NOT use the `@camunda8` SDK job
|
|
7
|
-
* worker:
|
|
8
|
-
* maxParallel − active` and
|
|
9
|
-
* shared across K types with per-type gating". The
|
|
10
|
-
* over a NARROW
|
|
7
|
+
* worker (`createJobWorker`): that convenience loop models one self-driving
|
|
8
|
+
* poller per type with `maxJobsToActivate = maxParallel − active` and cannot
|
|
9
|
+
* express "global capacity S shared across K types with per-type gating". The
|
|
10
|
+
* supervisor rolls its own race over a NARROW single-shot engine surface — but
|
|
11
|
+
* every call on that surface still PREFERS the injected `camunda` SDK client's
|
|
12
|
+
* typed method, hand-rolling REST only as a standalone/wire-test fallback:
|
|
11
13
|
*
|
|
12
|
-
* - `activate` →
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
14
|
+
* - `activate` → the SDK's typed `activateJobs` (operationId `activateJobs` →
|
|
15
|
+
* `POST <base>/jobs/activation`) for exactly ONE type, one long-poll,
|
|
16
|
+
* resolving 0..`maxJobsToActivate` jobs (0 == the long-poll expired empty).
|
|
17
|
+
* `activateJobs` is the SINGLE-SHOT primitive `createJobWorker` is built on;
|
|
18
|
+
* its input body (`JobActivationRequest`) is 1:1 with ours and its returned
|
|
19
|
+
* `CancelablePromise.cancel()` aborts the in-flight long-poll, so a losing
|
|
20
|
+
* `raceAll` fiber interrupts its poll at once. Falls back to the SAME call
|
|
21
|
+
* issued raw via `fetchImpl` when no SDK client is injected. `timeout` is the
|
|
22
|
+
* SHORT initial lock (the crash-safety net); `requestTimeout` is how long the
|
|
23
|
+
* call blocks server-side.
|
|
24
|
+
* - `extendLock` → the SDK's typed `updateJob` (operationId `updateJob`,
|
|
25
|
+
* `PATCH <base>/jobs/{jobKey}` with `{ changeset: { timeout } }`) when a
|
|
26
|
+
* `camunda` SDK client is injected, else the SAME call issued raw via
|
|
27
|
+
* `fetchImpl` (so the module stays wire-testable without a live client).
|
|
17
28
|
* The C8 contract SETs the lock to `ms` from now (a duration-from-now), which
|
|
18
29
|
* is exactly the supervisor's "extend the winner to the recovery window, then
|
|
19
|
-
* heartbeat" model — set, not accumulate.
|
|
30
|
+
* heartbeat" model — set, not accumulate. NOTE: there is no
|
|
31
|
+
* `/jobs/{jobKey}/timeout` sub-route — that was a drifted URL that 404s on the
|
|
32
|
+
* engine; the timeout is a `changeset` field on the job resource itself.
|
|
33
|
+
* - `complete` / `fail` → the SDK's typed `completeJob` / `failJob` (operationIds
|
|
34
|
+
* `completeJob` → `POST <base>/jobs/{jobKey}/completion`, `failJob` →
|
|
35
|
+
* `POST <base>/jobs/{jobKey}/failure`) when a `camunda` SDK client is injected,
|
|
36
|
+
* else the SAME calls issued raw via `fetchImpl` — the settle surface the
|
|
37
|
+
* supervisor's JobRunner uses once an agent finishes.
|
|
38
|
+
*
|
|
39
|
+
* Every method on this surface (`activate`→`activateJobs`,
|
|
40
|
+
* `extendLock`→`updateJob`, `complete`→`completeJob`, `fail`→`failJob`) PREFERS
|
|
41
|
+
* the injected `camunda` client and only hand-rolls the REST call as a
|
|
42
|
+
* standalone/wire-test fallback; there is no sanctioned raw-only method. This
|
|
43
|
+
* convention is pinned by `supervisor-engine-sdk-preference.test.mjs` — a new
|
|
44
|
+
* engine method that hits a raw route must either prefer an SDK method or be
|
|
45
|
+
* added to that test's raw-only allowlist with a reason.
|
|
20
46
|
*
|
|
21
47
|
* This module is the raw-JS analogue of `agentic-endpoint.mjs`: it is Effect-free
|
|
22
48
|
* (the supervisor's `makeEngineClient` lift wraps each method into the Effect
|
|
@@ -136,7 +162,8 @@ async function readErrorBody(res) {
|
|
|
136
162
|
* @param {Record<string,string>|(() => (Record<string,string>|Promise<Record<string,string>>))} [opts.authHeaders] Ready-made auth header map, OR a resolver invoked per request (so rotating SDK auth — e.g. an OAuth bearer that refreshes — is re-derived each call rather than frozen). Wins over `token`.
|
|
137
163
|
* @param {typeof fetch} [opts.fetchImpl] Injected `fetch` (defaults to the global; overridden in tests).
|
|
138
164
|
* @param {number} [opts.requestTimeoutSlackMs] Extra ms added to a call's abort budget over its server long-poll (default 5000).
|
|
139
|
-
* @
|
|
165
|
+
* @param {{ activateJobs?: (input: { type: string, worker?: string, maxJobsToActivate: number, timeout: number, requestTimeout?: number }) => (Promise<{ jobs?: object[] }> & { cancel?: () => void }), updateJob?: (req: { jobKey: string, changeset: { timeout: number } }) => Promise<unknown>, completeJob?: (req: { jobKey: string, variables?: object }) => Promise<unknown>, failJob?: (req: { jobKey: string, retries?: number, errorMessage?: string, retryBackOff?: number, variables?: object }) => Promise<unknown> }} [opts.camunda] Optional Camunda SDK client. When present, each engine method prefers its typed SDK method over the raw fetch fallback: `activate`→`activateJobs` (`POST /v2/jobs/activation`, cancelled via the returned `CancelablePromise.cancel()`), `extendLock`→`updateJob` (`PATCH /v2/jobs/{jobKey}` `{ changeset: { timeout } }`), `complete`→`completeJob` (`POST /v2/jobs/{jobKey}/completion`), `fail`→`failJob` (`POST /v2/jobs/{jobKey}/failure`).
|
|
166
|
+
* @returns {{ activate(req: ActivateRequest, signal?: AbortSignal): Promise<ReadonlyArray<ActivatedJob>>, extendLock(jobKey: string, ms: number): Promise<void>, complete(jobKey: string, variables?: object): Promise<void>, fail(jobKey: string, opts?: { retries?: number, errorMessage?: string, retryBackOff?: number, variables?: object }): Promise<void> }}
|
|
140
167
|
*/
|
|
141
168
|
export function createRawEngineClient(opts = {}) {
|
|
142
169
|
const {
|
|
@@ -146,6 +173,7 @@ export function createRawEngineClient(opts = {}) {
|
|
|
146
173
|
authHeaders,
|
|
147
174
|
fetchImpl = fetch,
|
|
148
175
|
requestTimeoutSlackMs = 5_000,
|
|
176
|
+
camunda,
|
|
149
177
|
} = opts;
|
|
150
178
|
if (typeof fetchImpl !== "function") {
|
|
151
179
|
throw new TypeError("createRawEngineClient: `fetchImpl` must be a function (global fetch or an injected fake)");
|
|
@@ -172,34 +200,78 @@ export function createRawEngineClient(opts = {}) {
|
|
|
172
200
|
|
|
173
201
|
/**
|
|
174
202
|
* Issue one call with an abort budget. `abortAfterMs <= 0` means no timer
|
|
175
|
-
* (the caller relies purely on the server long-poll / connection).
|
|
203
|
+
* (the caller relies purely on the server long-poll / connection). An optional
|
|
204
|
+
* `extSignal` (e.g. the Effect fiber's interruption signal for the `activate`
|
|
205
|
+
* long-poll) also aborts the in-flight fetch, so a losing `raceAll` fiber stops
|
|
206
|
+
* its poll immediately instead of leaking it until the budget timer fires.
|
|
176
207
|
*/
|
|
177
|
-
async function call(url, init, abortAfterMs) {
|
|
208
|
+
async function call(url, init, abortAfterMs, extSignal) {
|
|
178
209
|
const controller = new AbortController();
|
|
179
210
|
const timer = abortAfterMs > 0 ? setTimeout(() => controller.abort(), abortAfterMs) : null;
|
|
211
|
+
const onExtAbort = () => controller.abort();
|
|
212
|
+
if (extSignal) {
|
|
213
|
+
if (extSignal.aborted) controller.abort();
|
|
214
|
+
else extSignal.addEventListener("abort", onExtAbort, { once: true });
|
|
215
|
+
}
|
|
180
216
|
try {
|
|
181
217
|
const headers = await resolveHeaders();
|
|
182
218
|
return await fetchImpl(url, { ...init, headers, signal: controller.signal });
|
|
183
219
|
} finally {
|
|
184
220
|
if (timer) clearTimeout(timer);
|
|
221
|
+
if (extSignal) extSignal.removeEventListener("abort", onExtAbort);
|
|
185
222
|
}
|
|
186
223
|
}
|
|
187
224
|
|
|
188
225
|
return {
|
|
189
|
-
async activate(req) {
|
|
190
|
-
|
|
226
|
+
async activate(req, signal) {
|
|
227
|
+
// Prefer the SDK's typed `activateJobs` (operationId `activateJobs` →
|
|
228
|
+
// `POST /v2/jobs/activation`) so the ONE-type long-poll tracks the engine
|
|
229
|
+
// contract instead of a hand-rolled body/URL. The SDK's `createJobWorker`
|
|
230
|
+
// convenience loop can't express the supervisor's global-capacity single
|
|
231
|
+
// race — but `activateJobs` is the SAME single-shot primitive that loop is
|
|
232
|
+
// built on, and its input body (`JobActivationRequest`) is 1:1 with ours.
|
|
233
|
+
// Its `CancelablePromise.cancel()` aborts the in-flight long-poll, so a
|
|
234
|
+
// losing `raceAll` fiber (interruption → `signal.aborted`) stops its poll
|
|
235
|
+
// immediately. Fall back to the SAME call issued raw via `fetchImpl` when no
|
|
236
|
+
// SDK client is injected (keeps this module wire-testable and standalone).
|
|
237
|
+
// `timeout` is the SHORT initial lock applied to any returned job;
|
|
238
|
+
// `requestTimeout` is the server-side long-poll window.
|
|
239
|
+
const input = {
|
|
191
240
|
type: req.type,
|
|
192
241
|
worker,
|
|
193
242
|
maxJobsToActivate: req.maxJobsToActivate,
|
|
194
|
-
// `timeout` is the SHORT initial lock applied to any returned job.
|
|
195
243
|
timeout: req.lockMs,
|
|
196
|
-
// `requestTimeout` is the server-side long-poll window.
|
|
197
244
|
requestTimeout: req.requestTimeoutMs,
|
|
198
|
-
}
|
|
245
|
+
};
|
|
199
246
|
// Give the abort budget slack over the server long-poll so we don't cancel
|
|
200
247
|
// a still-valid long-poll a hair before the server would answer it.
|
|
201
248
|
const abortAfterMs = req.requestTimeoutMs > 0 ? req.requestTimeoutMs + requestTimeoutSlackMs : 0;
|
|
202
|
-
|
|
249
|
+
if (camunda && typeof camunda.activateJobs === "function") {
|
|
250
|
+
const p = camunda.activateJobs(input);
|
|
251
|
+
// Wire BOTH cancellation sources onto the CancelablePromise: the external
|
|
252
|
+
// interruption signal (fiber lost the race) and the client-side abort
|
|
253
|
+
// budget (a hung connection past the server window). `cancel()` aborts the
|
|
254
|
+
// SDK's own AbortController, ending the long-poll.
|
|
255
|
+
const cancel = typeof p?.cancel === "function" ? () => p.cancel() : () => {};
|
|
256
|
+
const timer = abortAfterMs > 0 ? setTimeout(cancel, abortAfterMs) : null;
|
|
257
|
+
const onExtAbort = () => cancel();
|
|
258
|
+
if (signal) {
|
|
259
|
+
if (signal.aborted) cancel();
|
|
260
|
+
else signal.addEventListener("abort", onExtAbort, { once: true });
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const result = await p;
|
|
264
|
+
const jobs = Array.isArray(result?.jobs) ? result.jobs : Array.isArray(result) ? result : [];
|
|
265
|
+
return jobs.map(mapJob);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
throw new Error(`activate ${req.type}: SDK activateJobs failed: ${err?.message ?? err}`, { cause: err });
|
|
268
|
+
} finally {
|
|
269
|
+
if (timer) clearTimeout(timer);
|
|
270
|
+
if (signal) signal.removeEventListener("abort", onExtAbort);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const body = JSON.stringify(input);
|
|
274
|
+
const res = await call(`${base}/jobs/activation`, { method: "POST", body }, abortAfterMs, signal);
|
|
203
275
|
if (!res || !res.ok) {
|
|
204
276
|
const status = res ? res.status : "?";
|
|
205
277
|
throw new Error(`activate ${req.type}: HTTP ${status} from ${base}/jobs/activation${res ? await readErrorBody(res) : ""}`);
|
|
@@ -210,8 +282,21 @@ export function createRawEngineClient(opts = {}) {
|
|
|
210
282
|
},
|
|
211
283
|
|
|
212
284
|
async extendLock(jobKey, ms) {
|
|
213
|
-
|
|
214
|
-
|
|
285
|
+
// Prefer the SDK's typed `updateJob` (operationId `updateJob` →
|
|
286
|
+
// `PATCH /v2/jobs/{jobKey}` with `{ changeset: { timeout } }`) so the lock
|
|
287
|
+
// extension tracks the engine contract instead of a hand-rolled URL. Fall
|
|
288
|
+
// back to the SAME call issued raw when no SDK client is injected (keeps
|
|
289
|
+
// this module wire-testable and usable standalone).
|
|
290
|
+
if (camunda && typeof camunda.updateJob === "function") {
|
|
291
|
+
try {
|
|
292
|
+
await camunda.updateJob({ changeset: { timeout: ms }, jobKey: String(jobKey) });
|
|
293
|
+
return;
|
|
294
|
+
} catch (err) {
|
|
295
|
+
throw new Error(`extendLock ${jobKey}: SDK updateJob failed: ${err?.message ?? err}`, { cause: err });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const url = `${base}/jobs/${encodeURIComponent(jobKey)}`;
|
|
299
|
+
const res = await call(url, { method: "PATCH", body: JSON.stringify({ changeset: { timeout: ms } }) }, 15_000);
|
|
215
300
|
if (!res || !res.ok) {
|
|
216
301
|
const status = res ? res.status : "?";
|
|
217
302
|
throw new Error(`extendLock ${jobKey}: HTTP ${status} from ${url}${res ? await readErrorBody(res) : ""}`);
|
|
@@ -220,21 +305,33 @@ export function createRawEngineClient(opts = {}) {
|
|
|
220
305
|
|
|
221
306
|
// ---- Job completion / failure ------------------------------------------
|
|
222
307
|
// The narrow surface the supervisor's JobRunner needs to SETTLE a job once
|
|
223
|
-
// its agent harness finishes — the
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
// `
|
|
229
|
-
//
|
|
308
|
+
// its agent harness finishes — the analogue of the SDK job object's
|
|
309
|
+
// `job.complete()` / `job.fail()` (which the per-type SDK poller path in
|
|
310
|
+
// `workAgent` still uses). A plain `ActivatedJob` (jobKey/type/variables)
|
|
311
|
+
// carries no settle methods, so the supervisor path settles via these calls.
|
|
312
|
+
// Like `extendLock`, each PREFERS the injected `camunda` SDK client's typed
|
|
313
|
+
// method (`completeJob` / `failJob`) and only hand-rolls the REST call as a
|
|
314
|
+
// standalone/wire-test fallback. Effect-free and wire-testable; a non-2xx /
|
|
315
|
+
// SDK rejection (e.g. a 409 when the lock lapsed and the job was reclaimed)
|
|
316
|
+
// surfaces as a rejected promise for the port to map.
|
|
230
317
|
|
|
231
318
|
async complete(jobKey, variables) {
|
|
232
|
-
//
|
|
319
|
+
// Prefer the SDK's typed `completeJob` (operationId `completeJob` →
|
|
320
|
+
// `POST /v2/jobs/{jobKey}/completion` with `{ variables }`) when a `camunda`
|
|
321
|
+
// SDK client is injected, else the SAME call issued raw via `fetchImpl`. The
|
|
233
322
|
// result-variable map the model produced is merged onto the process
|
|
234
323
|
// instance. C8 v2 answers 204 No Content on success.
|
|
324
|
+
const vars = isPlainObjectMap(variables) ? { variables } : {};
|
|
325
|
+
if (camunda && typeof camunda.completeJob === "function") {
|
|
326
|
+
try {
|
|
327
|
+
await camunda.completeJob({ jobKey: String(jobKey), ...vars });
|
|
328
|
+
return;
|
|
329
|
+
} catch (err) {
|
|
330
|
+
throw new Error(`complete ${jobKey}: SDK completeJob failed: ${err?.message ?? err}`, { cause: err });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
235
333
|
const url = `${base}/jobs/${encodeURIComponent(jobKey)}/completion`;
|
|
236
|
-
const
|
|
237
|
-
const res = await call(url, { method: "POST", body }, 15_000);
|
|
334
|
+
const res = await call(url, { method: "POST", body: JSON.stringify(vars) }, 15_000);
|
|
238
335
|
if (!res || !res.ok) {
|
|
239
336
|
const status = res ? res.status : "?";
|
|
240
337
|
throw new Error(`complete ${jobKey}: HTTP ${status} from ${url}${res ? await readErrorBody(res) : ""}`);
|
|
@@ -246,19 +343,30 @@ export function createRawEngineClient(opts = {}) {
|
|
|
246
343
|
// null-safe `variables`), so a caller passing `null` never trips the
|
|
247
344
|
// signature-destructure TypeError.
|
|
248
345
|
const { retries = 0, errorMessage, retryBackOff, variables } = opts || {};
|
|
249
|
-
// `POST <base>/v2/jobs/{jobKey}/failure` with `{ retries, errorMessage?,
|
|
250
|
-
// retryBackOff?, variables? }` — `retries > 0` re-queues for another
|
|
251
|
-
// attempt, `retries === 0` raises an incident. Optional fields are omitted
|
|
252
|
-
// when absent so the engine applies its own defaults. C8 v2 answers 204.
|
|
253
|
-
const url = `${base}/jobs/${encodeURIComponent(jobKey)}/failure`;
|
|
254
346
|
// Normalize retries to a non-negative integer (mirrors `mapJob`), so a
|
|
255
|
-
// string/float/negative never reaches the engine as an invalid
|
|
347
|
+
// string/float/negative never reaches the engine (or SDK) as an invalid
|
|
348
|
+
// count. `retries > 0` re-queues for another attempt, `retries === 0`
|
|
349
|
+
// raises an incident. Optional fields are omitted when absent so the engine
|
|
350
|
+
// applies its own defaults.
|
|
256
351
|
const nRetries = Number(retries);
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
if (
|
|
261
|
-
|
|
352
|
+
const normRetries = Number.isFinite(nRetries) ? Math.max(0, Math.trunc(nRetries)) : 0;
|
|
353
|
+
const extra = {};
|
|
354
|
+
if (errorMessage !== undefined && errorMessage !== null) extra.errorMessage = String(errorMessage);
|
|
355
|
+
if (Number.isFinite(retryBackOff) && retryBackOff > 0) extra.retryBackOff = retryBackOff;
|
|
356
|
+
if (isPlainObjectMap(variables)) extra.variables = variables;
|
|
357
|
+
// Prefer the SDK's typed `failJob` (operationId `failJob` →
|
|
358
|
+
// `POST /v2/jobs/{jobKey}/failure`) when a `camunda` SDK client is injected,
|
|
359
|
+
// else the SAME call issued raw via `fetchImpl`. C8 v2 answers 204.
|
|
360
|
+
if (camunda && typeof camunda.failJob === "function") {
|
|
361
|
+
try {
|
|
362
|
+
await camunda.failJob({ jobKey: String(jobKey), retries: normRetries, ...extra });
|
|
363
|
+
return;
|
|
364
|
+
} catch (err) {
|
|
365
|
+
throw new Error(`fail ${jobKey}: SDK failJob failed: ${err?.message ?? err}`, { cause: err });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const url = `${base}/jobs/${encodeURIComponent(jobKey)}/failure`;
|
|
369
|
+
const res = await call(url, { method: "POST", body: JSON.stringify({ retries: normRetries, ...extra }) }, 15_000);
|
|
262
370
|
if (!res || !res.ok) {
|
|
263
371
|
const status = res ? res.status : "?";
|
|
264
372
|
throw new Error(`fail ${jobKey}: HTTP ${status} from ${url}${res ? await readErrorBody(res) : ""}`);
|