@rebasepro/server 0.22.0 → 0.23.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/{GCSStorageController-CLIJXwGS.js → GCSStorageController-CjrA4PMo.js} +37 -23
- package/dist/GCSStorageController-CjrA4PMo.js.map +1 -0
- package/dist/{S3StorageController-Dcuf8lMA.js → S3StorageController-B6pKDNVj.js} +2 -2
- package/dist/{S3StorageController-Dcuf8lMA.js.map → S3StorageController-B6pKDNVj.js.map} +1 -1
- package/dist/api/ast-schema-editor.d.ts +35 -0
- package/dist/api/live-schema-routes.d.ts +14 -0
- package/dist/api/rest/api-generator.d.ts +117 -35
- package/dist/api/rest/auth-collection-writes.d.ts +85 -0
- package/dist/api/rest/field-access-query.d.ts +6 -2
- package/dist/api/rest/idempotency.d.ts +7 -1
- package/dist/api/rest/nested-write-access.d.ts +46 -0
- package/dist/api/rest/write-validation.d.ts +32 -0
- package/dist/{ast-schema-editor-CslO8Oje.js → ast-schema-editor-Mvr50v_S.js} +104 -4
- package/dist/ast-schema-editor-Mvr50v_S.js.map +1 -0
- package/dist/auth/address-ownership.d.ts +53 -0
- package/dist/auth/admin-user-ops.d.ts +35 -2
- package/dist/auth/auth-hooks.d.ts +4 -0
- package/dist/auth/captcha.d.ts +5 -0
- package/dist/auth/interfaces.d.ts +36 -6
- package/dist/auth/jwt.d.ts +17 -0
- package/dist/auth/oauth-signin-policy.d.ts +25 -8
- package/dist/auth/rate-limiter.d.ts +31 -1
- package/dist/auth/session-routes.d.ts +7 -0
- package/dist/auth/token-revocation.d.ts +4 -1
- package/dist/{auth-CCDpk2rn.js → auth-B-GIMpDG.js} +639 -228
- package/dist/auth-B-GIMpDG.js.map +1 -0
- package/dist/backend-DTAOsLQc.js +30 -0
- package/dist/backend-DTAOsLQc.js.map +1 -0
- package/dist/backup/backup-common.d.ts +19 -0
- package/dist/{backup-DzI9jLwc.js → backup-D7YR94N3.js} +69 -8
- package/dist/backup-D7YR94N3.js.map +1 -0
- package/dist/boot/driver.d.ts +10 -0
- package/dist/boot/env.d.ts +2 -2
- package/dist/boot/fetch-bundle.d.ts +18 -1
- package/dist/boot/rls-audit-option.d.ts +26 -0
- package/dist/boot/sources.d.ts +1 -0
- package/dist/{contract-routes-eLxV0le1.js → contract-routes-CbFjuBwa.js} +2 -2
- package/dist/{contract-routes-eLxV0le1.js.map → contract-routes-CbFjuBwa.js.map} +1 -1
- package/dist/cron/cron-routes.d.ts +7 -2
- package/dist/cron/cron-scheduler.d.ts +128 -8
- package/dist/cron/cron-store.d.ts +72 -8
- package/dist/cron/index.d.ts +1 -1
- package/dist/{cron-loader-CQjvjpEw.js → cron-loader-DfTj2Hbi.js} +2 -2
- package/dist/{cron-loader-CQjvjpEw.js.map → cron-loader-DfTj2Hbi.js.map} +1 -1
- package/dist/{cron-routes-B7CRGfiq.js → cron-routes-eE8nif_b.js} +33 -12
- package/dist/cron-routes-eE8nif_b.js.map +1 -0
- package/dist/{cron-scheduler-COPQxlEq.js → cron-scheduler-B0pLfAix.js} +394 -68
- package/dist/cron-scheduler-B0pLfAix.js.map +1 -0
- package/dist/{cron-store-BYGZFNWk.js → cron-store-TcoGz-xS.js} +133 -12
- package/dist/cron-store-TcoGz-xS.js.map +1 -0
- package/dist/{ddl-bootstrap-CfNvxMuK.js → ddl-bootstrap-C6mo0Kmz.js} +2 -25
- package/dist/ddl-bootstrap-C6mo0Kmz.js.map +1 -0
- package/dist/email/link-base.d.ts +5 -4
- package/dist/email/smtp-email-service.d.ts +13 -1
- package/dist/email/templates.d.ts +9 -0
- package/dist/email/types.d.ts +3 -2
- package/dist/env.d.ts +24 -5
- package/dist/{history-recorder-BQmB0P_j.js → history-recorder-B4MpJfJK.js} +8 -6
- package/dist/history-recorder-B4MpJfJK.js.map +1 -0
- package/dist/{history-store-CetkrBBD.js → history-store-BhxWOuz9.js} +2 -2
- package/dist/{history-store-CetkrBBD.js.map → history-store-BhxWOuz9.js.map} +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.es.js +2717 -866
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +5 -2
- package/dist/init/shutdown.d.ts +8 -3
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/job-queue.d.ts +23 -2
- package/dist/jobs/job-store.d.ts +37 -5
- package/dist/jobs/types.d.ts +8 -6
- package/dist/{jobs-Bjr8DZAi.js → jobs-CazMYhyy.js} +305 -166
- package/dist/jobs-CazMYhyy.js.map +1 -0
- package/dist/{jwt-C4OW-DNq.js → jwt-DnQHNFCl.js} +77 -25
- package/dist/{jwt-C4OW-DNq.js.map → jwt-DnQHNFCl.js.map} +1 -1
- package/dist/{keys-Qfc4XieN.js → keys-CogCQpxG.js} +18 -3
- package/dist/{keys-Qfc4XieN.js.map → keys-CogCQpxG.js.map} +1 -1
- package/dist/{logs-routes-3EEzPjhl.js → logs-routes-Bj4TYYUl.js} +7 -4
- package/dist/{logs-routes-3EEzPjhl.js.map → logs-routes-Bj4TYYUl.js.map} +1 -1
- package/dist/mcp/mcp-routes.d.ts +38 -2
- package/dist/mcp/mcp-tools.d.ts +7 -1
- package/dist/mcp/oauth-routes.d.ts +27 -0
- package/dist/mcp/oauth-store.d.ts +29 -13
- package/dist/metrics/history-recorder.d.ts +1 -1
- package/dist/{openapi-generator-D8uFz-LW.js → openapi-generator-O_O24MAT.js} +34 -12
- package/dist/openapi-generator-O_O24MAT.js.map +1 -0
- package/dist/{query-parser-BleZmY18.js → query-parser-DGRVFNM3.js} +39 -30
- package/dist/query-parser-DGRVFNM3.js.map +1 -0
- package/dist/rls-audit/index.d.ts +4 -0
- package/dist/{schema-editor-routes-DdLihzp0.js → schema-editor-routes-C5-lh_jO.js} +2 -2
- package/dist/{schema-editor-routes-DdLihzp0.js.map → schema-editor-routes-C5-lh_jO.js.map} +1 -1
- package/dist/{src-1vL-I1Po.js → src-pmvW7BFx.js} +122 -14
- package/dist/src-pmvW7BFx.js.map +1 -0
- package/dist/{src-Br6ARbs6.js → src-vkcwKXbT.js} +2 -27
- package/dist/{src-Br6ARbs6.js.map → src-vkcwKXbT.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +11 -1
- package/dist/storage/keys.d.ts +12 -0
- package/dist/storage/rendition-cache.d.ts +11 -1
- package/dist/storage/request-keys.d.ts +67 -0
- package/dist/storage/types.d.ts +17 -1
- package/dist/types-BfKcm9do.js.map +1 -1
- package/package.json +9 -9
- package/dist/GCSStorageController-CLIJXwGS.js.map +0 -1
- package/dist/ast-schema-editor-CslO8Oje.js.map +0 -1
- package/dist/auth-CCDpk2rn.js.map +0 -1
- package/dist/backup-DzI9jLwc.js.map +0 -1
- package/dist/cron-routes-B7CRGfiq.js.map +0 -1
- package/dist/cron-scheduler-COPQxlEq.js.map +0 -1
- package/dist/cron-store-BYGZFNWk.js.map +0 -1
- package/dist/ddl-bootstrap-CfNvxMuK.js.map +0 -1
- package/dist/history-recorder-BQmB0P_j.js.map +0 -1
- package/dist/jobs-Bjr8DZAi.js.map +0 -1
- package/dist/openapi-generator-D8uFz-LW.js.map +0 -1
- package/dist/query-parser-BleZmY18.js.map +0 -1
- package/dist/src-1vL-I1Po.js.map +0 -1
|
@@ -3,8 +3,10 @@ import __rebaseProcess from "process";
|
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
5
|
import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
|
|
6
|
-
import "./src-
|
|
6
|
+
import "./src-vkcwKXbT.js";
|
|
7
7
|
import { a as redactSensitiveText, r as logger } from "./logger-DO2PZc4i.js";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { hostname } from "node:os";
|
|
8
10
|
//#region ../types/src/types/env_boolean.ts
|
|
9
11
|
var YES = /* @__PURE__ */ new Set([
|
|
10
12
|
"true",
|
|
@@ -148,6 +150,7 @@ function buildScaleToZeroWarning(jobs, env = process.env) {
|
|
|
148
150
|
var cron_scheduler_exports = /* @__PURE__ */ __exportAll({
|
|
149
151
|
CronScheduler: () => CronScheduler,
|
|
150
152
|
findMostRecentSlot: () => findMostRecentSlot,
|
|
153
|
+
isAlreadyExecutingSkip: () => isAlreadyExecutingSkip,
|
|
151
154
|
isValidTimeZone: () => isValidTimeZone,
|
|
152
155
|
parseCronExpression: () => parseCronExpression,
|
|
153
156
|
validateCronExpression: () => validateCronExpression
|
|
@@ -326,7 +329,6 @@ function matchesCronFields(candidate, fields, zone) {
|
|
|
326
329
|
};
|
|
327
330
|
return fields.months.includes(wall.month) && fields.doms.includes(wall.dom) && fields.dows.includes(wall.dow) && fields.hours.includes(wall.hour) && fields.minutes.includes(wall.minute);
|
|
328
331
|
}
|
|
329
|
-
/** ~1 year in minutes — the walk bound for both search directions. */
|
|
330
332
|
/**
|
|
331
333
|
* How far forward to look for the next matching slot.
|
|
332
334
|
*
|
|
@@ -335,6 +337,7 @@ function matchesCronFields(candidate, fields, zone) {
|
|
|
335
337
|
* search never found it.
|
|
336
338
|
*/
|
|
337
339
|
var MAX_SLOT_SEARCH_MINUTES = 2105280;
|
|
340
|
+
var MINUTE_MS = 6e4;
|
|
338
341
|
/**
|
|
339
342
|
* Calculate the next Date after `after` that matches the cron expression.
|
|
340
343
|
* Throws on invalid expressions.
|
|
@@ -342,11 +345,11 @@ var MAX_SLOT_SEARCH_MINUTES = 2105280;
|
|
|
342
345
|
function parseCronExpression(expression, after, timezone) {
|
|
343
346
|
const fields = parseCronFields(expression);
|
|
344
347
|
const candidate = new Date(after);
|
|
345
|
-
candidate.
|
|
346
|
-
candidate.
|
|
348
|
+
candidate.setUTCSeconds(0, 0);
|
|
349
|
+
candidate.setTime(candidate.getTime() + MINUTE_MS);
|
|
347
350
|
for (let i = 0; i < MAX_SLOT_SEARCH_MINUTES; i++) {
|
|
348
351
|
if (matchesCronFields(candidate, fields, timezone)) return candidate;
|
|
349
|
-
candidate.
|
|
352
|
+
candidate.setTime(candidate.getTime() + MINUTE_MS);
|
|
350
353
|
}
|
|
351
354
|
throw new Error(`Cron expression "${expression}" has no matching time within ${Math.round(MAX_SLOT_SEARCH_MINUTES / 525960)} years of ${after.toISOString()}. Check the day-of-month and month fields — a date such as 31 February never occurs.`);
|
|
352
355
|
}
|
|
@@ -369,10 +372,10 @@ function parseCronExpression(expression, after, timezone) {
|
|
|
369
372
|
function findMostRecentSlot(expression, from, to, timezone) {
|
|
370
373
|
const fields = parseCronFields(expression);
|
|
371
374
|
const candidate = new Date(to);
|
|
372
|
-
candidate.
|
|
375
|
+
candidate.setUTCSeconds(0, 0);
|
|
373
376
|
for (let i = 0; i < MAX_SLOT_SEARCH_MINUTES && candidate.getTime() >= from.getTime(); i++) {
|
|
374
377
|
if (matchesCronFields(candidate, fields, timezone)) return candidate;
|
|
375
|
-
candidate.
|
|
378
|
+
candidate.setTime(candidate.getTime() - MINUTE_MS);
|
|
376
379
|
}
|
|
377
380
|
}
|
|
378
381
|
var MAX_LOGS_PER_JOB = 50;
|
|
@@ -387,6 +390,66 @@ var MIN_SCHEDULE_INTERVAL_MS = 5e3;
|
|
|
387
390
|
* further out than this must be reached in hops rather than one timer.
|
|
388
391
|
*/
|
|
389
392
|
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
393
|
+
/**
|
|
394
|
+
* `setTimeout` for a delay that may exceed {@link MAX_TIMER_DELAY_MS}: armed in
|
|
395
|
+
* hops no longer than the ceiling, re-measured against the deadline on each
|
|
396
|
+
* wake. Returns the function that cancels it.
|
|
397
|
+
*/
|
|
398
|
+
function setLongTimeout(callback, delayMs) {
|
|
399
|
+
const deadline = Date.now() + delayMs;
|
|
400
|
+
let handle;
|
|
401
|
+
const arm = () => {
|
|
402
|
+
handle = setTimeout(() => {
|
|
403
|
+
if (Date.now() < deadline) arm();
|
|
404
|
+
else callback();
|
|
405
|
+
}, Math.min(Math.max(deadline - Date.now(), 0), MAX_TIMER_DELAY_MS));
|
|
406
|
+
};
|
|
407
|
+
arm();
|
|
408
|
+
return () => clearTimeout(handle);
|
|
409
|
+
}
|
|
410
|
+
/** A run's timeout when its definition names none. */
|
|
411
|
+
var DEFAULT_TIMEOUT_SECONDS = 300;
|
|
412
|
+
/**
|
|
413
|
+
* How long a run lease outlasts the run's own timeout.
|
|
414
|
+
*
|
|
415
|
+
* The lease is timed on the database's clock from the moment it is taken, the
|
|
416
|
+
* timeout on this process's from a moment later; without a margin a lease
|
|
417
|
+
* could lapse a beat before its run is aborted and released, and another
|
|
418
|
+
* process start the job alongside it.
|
|
419
|
+
*/
|
|
420
|
+
var LEASE_GRACE_SECONDS = 30;
|
|
421
|
+
/**
|
|
422
|
+
* The lease a run with no timeout (`timeoutSeconds: Infinity`) holds.
|
|
423
|
+
*
|
|
424
|
+
* A lease is released when its run ends, so this only matters in two cases.
|
|
425
|
+
* The holder crashed: the job is blocked — every trigger answered "already
|
|
426
|
+
* executing", every slot skipped — until the lease lapses, so it must lapse
|
|
427
|
+
* within an hour rather than never. The run is still going after an hour:
|
|
428
|
+
* from then on another process may start the job beside it. A job that
|
|
429
|
+
* legitimately runs for hours should say so with a finite `timeoutSeconds`,
|
|
430
|
+
* which its lease then follows.
|
|
431
|
+
*/
|
|
432
|
+
var MAX_UNBOUNDED_LEASE_SECONDS = 3600;
|
|
433
|
+
/**
|
|
434
|
+
* Whether a log entry records a run that did not happen because one was
|
|
435
|
+
* already executing — on this process, or on another that holds the job's run
|
|
436
|
+
* lease. The shape is the documented marker of a skip, the one the Studio
|
|
437
|
+
* panel reads.
|
|
438
|
+
*/
|
|
439
|
+
function isAlreadyExecutingSkip(entry) {
|
|
440
|
+
const result = entry.result;
|
|
441
|
+
return typeof result === "object" && result !== null && "skipped" in result && result.skipped === true && "reason" in result && result.reason === "already_executing";
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Why `timeoutSeconds` cannot be used, or `undefined` when it can. Infinity is
|
|
445
|
+
* a timeout that never fires; zero, a negative number or NaN would fail every
|
|
446
|
+
* run the moment it started.
|
|
447
|
+
*/
|
|
448
|
+
function invalidTimeoutReason(timeoutSeconds) {
|
|
449
|
+
if (timeoutSeconds === void 0 || timeoutSeconds === Infinity) return void 0;
|
|
450
|
+
if (typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0) return void 0;
|
|
451
|
+
return `timeoutSeconds must be a positive number of seconds, or Infinity for none — got ${String(timeoutSeconds)}`;
|
|
452
|
+
}
|
|
390
453
|
var CronScheduler = class {
|
|
391
454
|
jobs = /* @__PURE__ */ new Map();
|
|
392
455
|
rejected = /* @__PURE__ */ new Map();
|
|
@@ -394,6 +457,19 @@ var CronScheduler = class {
|
|
|
394
457
|
store;
|
|
395
458
|
client;
|
|
396
459
|
/**
|
|
460
|
+
* Work `stop()` has to wait for: every run executing right now, and every
|
|
461
|
+
* run's log write still on its way to the store.
|
|
462
|
+
*/
|
|
463
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
464
|
+
/** The controller that ends each executing run — what `stop()` aborts. */
|
|
465
|
+
runControllers = /* @__PURE__ */ new Set();
|
|
466
|
+
/**
|
|
467
|
+
* This process, as a run lease names it: what "already executing on …"
|
|
468
|
+
* tells an operator. Each run adds its own suffix, so a release can only
|
|
469
|
+
* ever clear the lease that run took.
|
|
470
|
+
*/
|
|
471
|
+
instanceLabel = `${hostname()}:${process.pid}`;
|
|
472
|
+
/**
|
|
397
473
|
* Set the server singleton to make it available to cron job handlers.
|
|
398
474
|
*
|
|
399
475
|
* `RebaseServerClient`, not `RebaseClient`: the object `init.ts` passes is
|
|
@@ -445,13 +521,24 @@ var CronScheduler = class {
|
|
|
445
521
|
});
|
|
446
522
|
continue;
|
|
447
523
|
}
|
|
524
|
+
const timeoutProblem = invalidTimeoutReason(loaded.definition.timeoutSeconds);
|
|
525
|
+
if (timeoutProblem) {
|
|
526
|
+
logger.error(`[cron] Rejecting job "${loaded.id}": ${timeoutProblem}.`);
|
|
527
|
+
this.rejected.set(loaded.id, {
|
|
528
|
+
id: loaded.id,
|
|
529
|
+
name: loaded.definition.name ?? loaded.id,
|
|
530
|
+
schedule: loaded.definition.schedule,
|
|
531
|
+
reason: timeoutProblem
|
|
532
|
+
});
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
448
535
|
this.rejected.delete(loaded.id);
|
|
449
536
|
if (this.jobs.get(loaded.id)) {
|
|
450
537
|
logger.warn(`[cron] Duplicate cron job id: "${loaded.id}". Overwriting.`);
|
|
451
538
|
this.stopJob(loaded.id);
|
|
452
539
|
}
|
|
453
540
|
const enabled = loaded.definition.enabled !== false;
|
|
454
|
-
|
|
541
|
+
const job = {
|
|
455
542
|
id: loaded.id,
|
|
456
543
|
definition: loaded.definition,
|
|
457
544
|
enabled,
|
|
@@ -460,8 +547,9 @@ var CronScheduler = class {
|
|
|
460
547
|
totalFailures: 0,
|
|
461
548
|
logs: [],
|
|
462
549
|
executing: false
|
|
463
|
-
}
|
|
464
|
-
|
|
550
|
+
};
|
|
551
|
+
this.jobs.set(loaded.id, job);
|
|
552
|
+
if (this.started && this.shouldArm(job)) this.scheduleNext(loaded.id);
|
|
465
553
|
}
|
|
466
554
|
}
|
|
467
555
|
/**
|
|
@@ -482,21 +570,42 @@ var CronScheduler = class {
|
|
|
482
570
|
}).catch((err) => {
|
|
483
571
|
logger.warn("[cron] Failed to seed job stats from database", { error: err });
|
|
484
572
|
});
|
|
485
|
-
for (const [id, job] of this.jobs) if (job
|
|
573
|
+
for (const [id, job] of this.jobs) if (this.shouldArm(job)) this.scheduleNext(id);
|
|
486
574
|
if (!this.store) logger.warn("[cron] No cron store attached — runs are uncoordinated; with multiple app instances every instance will execute every job");
|
|
487
575
|
this.warnIfScaleToZero();
|
|
488
576
|
this.catchUpMissedSlots();
|
|
489
577
|
logger.info(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);
|
|
490
578
|
}
|
|
491
579
|
/**
|
|
492
|
-
* Stop the scheduler
|
|
580
|
+
* Stop the scheduler: clear every timer, then wait for the runs executing
|
|
581
|
+
* right now — for at most `timeoutMs` when given.
|
|
582
|
+
*
|
|
583
|
+
* A run still going when the budget runs out has its `ctx.signal` aborted
|
|
584
|
+
* and ends there, recorded as a failure that says why. Its slot stays
|
|
585
|
+
* claimed, so no other instance re-runs it; a handler that must not be cut
|
|
586
|
+
* short should watch its signal and leave its work resumable.
|
|
493
587
|
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
588
|
+
* Nothing is scheduled after `stop()`, whether or not the returned promise
|
|
589
|
+
* is awaited: the timers are cleared before it first yields.
|
|
496
590
|
*/
|
|
497
|
-
stop() {
|
|
591
|
+
async stop(timeoutMs) {
|
|
498
592
|
this.started = false;
|
|
499
593
|
for (const [id] of this.jobs) this.stopJob(id);
|
|
594
|
+
if (this.inFlight.size === 0) return;
|
|
595
|
+
const settled = Promise.allSettled([...this.inFlight]).then(() => true);
|
|
596
|
+
if (timeoutMs === void 0) {
|
|
597
|
+
await settled;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
let budget;
|
|
601
|
+
const finished = await Promise.race([settled, new Promise((resolve) => {
|
|
602
|
+
budget = setTimeout(() => resolve(false), timeoutMs);
|
|
603
|
+
if (budget && typeof budget === "object" && "unref" in budget) budget.unref();
|
|
604
|
+
})]);
|
|
605
|
+
clearTimeout(budget);
|
|
606
|
+
if (finished || this.runControllers.size === 0) return;
|
|
607
|
+
logger.warn(`[cron] ${this.runControllers.size} run(s) still executing after ${timeoutMs}ms — aborting them`);
|
|
608
|
+
for (const controller of this.runControllers) controller.abort(/* @__PURE__ */ new Error("The server is shutting down; the run was stopped before it finished"));
|
|
500
609
|
}
|
|
501
610
|
/**
|
|
502
611
|
* List all registered jobs with their current status.
|
|
@@ -522,6 +631,31 @@ var CronScheduler = class {
|
|
|
522
631
|
return job ? this.toStatus(job) : void 0;
|
|
523
632
|
}
|
|
524
633
|
/**
|
|
634
|
+
* {@link listJobs}, as the fleet sees it rather than this process alone.
|
|
635
|
+
*
|
|
636
|
+
* Reads the store: every job's enabled state — a pause another replica
|
|
637
|
+
* made — and whether another process holds its run lease. On a process
|
|
638
|
+
* whose scheduler is not started (the `api` role) it also reads each job's
|
|
639
|
+
* run count and last run from `cron_logs`, since that process runs nothing
|
|
640
|
+
* and its own counters would say 0 runs for a job the worker has run a
|
|
641
|
+
* thousand times.
|
|
642
|
+
*
|
|
643
|
+
* A store that cannot answer leaves this process's own view, with a
|
|
644
|
+
* warning: a listing is not worth failing.
|
|
645
|
+
*/
|
|
646
|
+
async fetchJobs() {
|
|
647
|
+
const jobs = [...this.jobs.values()];
|
|
648
|
+
const views = await this.readFleet(jobs);
|
|
649
|
+
return jobs.map((job) => this.toStatus(job, views.get(job.id)));
|
|
650
|
+
}
|
|
651
|
+
/** {@link getJob}, as {@link fetchJobs} reads it. */
|
|
652
|
+
async fetchJob(id) {
|
|
653
|
+
const job = this.jobs.get(id);
|
|
654
|
+
if (!job) return void 0;
|
|
655
|
+
const views = await this.readFleet([job]);
|
|
656
|
+
return this.toStatus(job, views.get(id));
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
525
659
|
* Get log entries for a job.
|
|
526
660
|
*/
|
|
527
661
|
getJobLogs(id, limit) {
|
|
@@ -542,27 +676,49 @@ var CronScheduler = class {
|
|
|
542
676
|
return this.getJobLogs(id, limit);
|
|
543
677
|
}
|
|
544
678
|
/**
|
|
545
|
-
* Enable or disable a job
|
|
679
|
+
* Enable or disable a job in this process only. `null` returns it to what
|
|
680
|
+
* its code declares.
|
|
681
|
+
*
|
|
682
|
+
* {@link persistJobEnabled} is what the admin API calls: this alone is
|
|
683
|
+
* forgotten on restart and reaches no other replica.
|
|
546
684
|
*/
|
|
547
685
|
setJobEnabled(id, enabled) {
|
|
548
686
|
const job = this.jobs.get(id);
|
|
549
687
|
if (!job) return void 0;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
} else if (!enabled) {
|
|
555
|
-
this.stopJob(id);
|
|
556
|
-
job.state = "disabled";
|
|
557
|
-
}
|
|
688
|
+
const effective = enabled ?? this.codeEnabled(job);
|
|
689
|
+
this.applyEnabled(job, effective);
|
|
690
|
+
if (effective && this.started) this.scheduleNext(id);
|
|
691
|
+
else if (!this.shouldArm(job)) this.stopJob(id);
|
|
558
692
|
return this.toStatus(job);
|
|
559
693
|
}
|
|
560
694
|
/**
|
|
695
|
+
* Enable or disable a job for every process that runs it, and keep it that
|
|
696
|
+
* way across restarts and redeploys: `true` or `false` overrides the code's
|
|
697
|
+
* `enabled`, `null` hands the decision back to it.
|
|
698
|
+
*
|
|
699
|
+
* The override is written to the store first, and only then applied here,
|
|
700
|
+
* so a write that fails throws and changes nothing — a pause that only the
|
|
701
|
+
* replica serving the request would honour is the failure this exists to
|
|
702
|
+
* end. Every scheduler reads the override when a slot comes due, before it
|
|
703
|
+
* claims the slot.
|
|
704
|
+
*
|
|
705
|
+
* Without a store that keeps the state (a driver with no SQL, persistence
|
|
706
|
+
* switched off, a custom store) this is {@link setJobEnabled}: local, and
|
|
707
|
+
* gone on restart.
|
|
708
|
+
*/
|
|
709
|
+
async persistJobEnabled(id, enabled, updatedBy) {
|
|
710
|
+
if (!this.jobs.has(id)) return void 0;
|
|
711
|
+
if (this.store?.saveEnabledOverride) await this.store.saveEnabledOverride(id, enabled, updatedBy);
|
|
712
|
+
return this.setJobEnabled(id, enabled);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
561
715
|
* Manually trigger a job execution immediately.
|
|
562
716
|
*
|
|
563
717
|
* Returns `undefined` if the job doesn't exist.
|
|
564
|
-
* If the job is currently executing,
|
|
565
|
-
* a `skipped: true`
|
|
718
|
+
* If the job is currently executing — here, or on another process that
|
|
719
|
+
* holds its run lease — returns the log entry with a `skipped: true`
|
|
720
|
+
* result rather than running concurrently; {@link isAlreadyExecutingSkip}
|
|
721
|
+
* recognises it.
|
|
566
722
|
*/
|
|
567
723
|
async triggerJob(id) {
|
|
568
724
|
const job = this.jobs.get(id);
|
|
@@ -587,8 +743,9 @@ var CronScheduler = class {
|
|
|
587
743
|
* `success: true` is deliberate: nothing failed. The `result.skipped` flag
|
|
588
744
|
* and the reason are what distinguishes it, and the Studio panel reads them.
|
|
589
745
|
*/
|
|
590
|
-
recordSkip(job, reason, manual) {
|
|
746
|
+
recordSkip(job, reason, manual, runningOn) {
|
|
591
747
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
748
|
+
const why = reason === "already_executing" ? "the previous run has not finished" : reason;
|
|
592
749
|
const logEntry = {
|
|
593
750
|
jobId: job.id,
|
|
594
751
|
startedAt: now,
|
|
@@ -599,16 +756,143 @@ var CronScheduler = class {
|
|
|
599
756
|
skipped: true,
|
|
600
757
|
reason
|
|
601
758
|
},
|
|
602
|
-
logs: [`Skipped: ${
|
|
759
|
+
logs: [`Skipped: ${why}${runningOn ? ` — it is running on ${runningOn}` : ""}`],
|
|
603
760
|
manual
|
|
604
761
|
};
|
|
605
762
|
job.logs.push(logEntry);
|
|
606
763
|
if (job.logs.length > MAX_LOGS_PER_JOB) job.logs.shift();
|
|
607
|
-
this.store
|
|
764
|
+
if (this.store) this.track(this.store.insertLog(logEntry).catch((persistErr) => {
|
|
608
765
|
logger.error(`[cron] Failed to persist skip for "${job.id}"`, { error: persistErr });
|
|
609
|
-
});
|
|
766
|
+
}));
|
|
610
767
|
return logEntry;
|
|
611
768
|
}
|
|
769
|
+
/** What the job's own definition says, before any override. */
|
|
770
|
+
codeEnabled(job) {
|
|
771
|
+
return job.definition.enabled !== false;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Whether the enabled state lives in the store, where every process reads
|
|
775
|
+
* it, rather than in this process alone.
|
|
776
|
+
*/
|
|
777
|
+
sharesEnabledState() {
|
|
778
|
+
return Boolean(this.store?.fetchJobStates && this.store.saveEnabledOverride);
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Whether a job gets a timer here. A job paused in this process's memory
|
|
782
|
+
* needs none — nothing but this process can resume it. A job whose state
|
|
783
|
+
* the store keeps does, paused or not: another replica may resume it, and
|
|
784
|
+
* reading the state when its slot comes due is how this one finds out.
|
|
785
|
+
*/
|
|
786
|
+
shouldArm(job) {
|
|
787
|
+
return job.enabled || this.sharesEnabledState();
|
|
788
|
+
}
|
|
789
|
+
/** Set a job's enabled flag, and its state with it unless a run is in progress. */
|
|
790
|
+
applyEnabled(job, enabled) {
|
|
791
|
+
job.enabled = enabled;
|
|
792
|
+
if (job.executing) return;
|
|
793
|
+
if (!enabled) job.state = "disabled";
|
|
794
|
+
else if (job.state === "disabled") job.state = "idle";
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Whether a job may run now: the store's override when it keeps one, else
|
|
798
|
+
* the code's `enabled`.
|
|
799
|
+
*
|
|
800
|
+
* One query per fire, before the slot is claimed — a paused job does not
|
|
801
|
+
* spend its slot. A store that cannot answer falls back to the code's
|
|
802
|
+
* `enabled`, with a warning: a scheduled run fails open, as it does when
|
|
803
|
+
* the claims table cannot answer, because a broken state table must not
|
|
804
|
+
* silently stop every job.
|
|
805
|
+
*/
|
|
806
|
+
async isEnabledNow(job) {
|
|
807
|
+
if (!this.store?.fetchJobStates || !this.sharesEnabledState()) return job.enabled;
|
|
808
|
+
try {
|
|
809
|
+
const enabled = (await this.store.fetchJobStates([job.id])).get(job.id)?.enabled ?? this.codeEnabled(job);
|
|
810
|
+
this.applyEnabled(job, enabled);
|
|
811
|
+
return enabled;
|
|
812
|
+
} catch (err) {
|
|
813
|
+
const enabled = this.codeEnabled(job);
|
|
814
|
+
logger.warn(`[cron] Could not read the enabled state of "${job.id}" — falling back to the code's enabled (${enabled}); a pause made from the admin API is not being honoured`, { error: err });
|
|
815
|
+
return enabled;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* How long a run's lease lasts: the run's timeout and a grace, so that a
|
|
820
|
+
* crashed holder frees the job once its run would have been aborted
|
|
821
|
+
* anyway. No timeout gets {@link MAX_UNBOUNDED_LEASE_SECONDS}.
|
|
822
|
+
*/
|
|
823
|
+
leaseSeconds(job) {
|
|
824
|
+
const timeout = job.definition.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
825
|
+
return Number.isFinite(timeout) ? timeout + LEASE_GRACE_SECONDS : MAX_UNBOUNDED_LEASE_SECONDS;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Take the job's run lease, the cross-process half of "already
|
|
829
|
+
* executing". A store without leases, or one that cannot answer, lets the
|
|
830
|
+
* run go ahead unleased — with a warning in the second case — the same way
|
|
831
|
+
* a claim that cannot answer does.
|
|
832
|
+
*/
|
|
833
|
+
async acquireLease(job) {
|
|
834
|
+
if (!this.store?.tryAcquireRunLease) return { acquired: true };
|
|
835
|
+
const token = `${this.instanceLabel}#${randomUUID().slice(0, 8)}`;
|
|
836
|
+
try {
|
|
837
|
+
const lease = await this.store.tryAcquireRunLease(job.id, token, this.leaseSeconds(job));
|
|
838
|
+
return lease.acquired ? {
|
|
839
|
+
acquired: true,
|
|
840
|
+
token
|
|
841
|
+
} : lease;
|
|
842
|
+
} catch (err) {
|
|
843
|
+
logger.warn(`[cron] Could not take the run lease for "${job.id}" — running without it, so another process may run it at the same time`, { error: err });
|
|
844
|
+
return { acquired: true };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
/** Release a lease this run took. A release that fails leaves it to lapse. */
|
|
848
|
+
async releaseLease(job, token) {
|
|
849
|
+
try {
|
|
850
|
+
await this.store?.releaseRunLease?.(job.id, token);
|
|
851
|
+
} catch (err) {
|
|
852
|
+
logger.warn(`[cron] Could not release the run lease for "${job.id}" — it lapses on its own within ${this.leaseSeconds(job)}s`, { error: err });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* What the store says about `jobs` beyond this process — see
|
|
857
|
+
* {@link fetchJobs}. Applies the enabled state it reads, and returns the
|
|
858
|
+
* rest as a view for {@link toStatus}. Never throws.
|
|
859
|
+
*/
|
|
860
|
+
async readFleet(jobs) {
|
|
861
|
+
const views = /* @__PURE__ */ new Map();
|
|
862
|
+
const viewOf = (id) => {
|
|
863
|
+
let view = views.get(id);
|
|
864
|
+
if (!view) {
|
|
865
|
+
view = {};
|
|
866
|
+
views.set(id, view);
|
|
867
|
+
}
|
|
868
|
+
return view;
|
|
869
|
+
};
|
|
870
|
+
const store = this.store;
|
|
871
|
+
const ids = jobs.map((job) => job.id);
|
|
872
|
+
if (store?.fetchJobStates && this.sharesEnabledState() && ids.length > 0) try {
|
|
873
|
+
const states = await store.fetchJobStates(ids);
|
|
874
|
+
for (const job of jobs) {
|
|
875
|
+
const state = states.get(job.id);
|
|
876
|
+
this.applyEnabled(job, state?.enabled ?? this.codeEnabled(job));
|
|
877
|
+
if (state?.runningBy && !job.executing) viewOf(job.id).runningElsewhere = true;
|
|
878
|
+
}
|
|
879
|
+
} catch (err) {
|
|
880
|
+
logger.warn("[cron] Could not read the jobs' shared state — listing what this process knows", { error: err });
|
|
881
|
+
}
|
|
882
|
+
if (!this.started) {
|
|
883
|
+
const now = /* @__PURE__ */ new Date();
|
|
884
|
+
for (const job of jobs) try {
|
|
885
|
+
viewOf(job.id).nextRunAt = parseCronExpression(job.definition.schedule, now, job.definition.timezone);
|
|
886
|
+
} catch {}
|
|
887
|
+
if (store?.fetchRunSummaries && ids.length > 0) try {
|
|
888
|
+
const summaries = await store.fetchRunSummaries(ids);
|
|
889
|
+
for (const [id, summary] of summaries) viewOf(id).summary = summary;
|
|
890
|
+
} catch (err) {
|
|
891
|
+
logger.warn("[cron] Could not read the jobs' run history — listing what this process knows", { error: err });
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return views;
|
|
895
|
+
}
|
|
612
896
|
/**
|
|
613
897
|
* Warn once at start when the process looks like it is running on a
|
|
614
898
|
* platform that freezes or evicts instances between requests, where the
|
|
@@ -638,7 +922,7 @@ var CronScheduler = class {
|
|
|
638
922
|
*/
|
|
639
923
|
scheduleNext(id) {
|
|
640
924
|
const job = this.jobs.get(id);
|
|
641
|
-
if (!job || !
|
|
925
|
+
if (!job || !this.started || !this.shouldArm(job)) return;
|
|
642
926
|
this.stopJob(id);
|
|
643
927
|
try {
|
|
644
928
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -648,21 +932,26 @@ var CronScheduler = class {
|
|
|
648
932
|
const delay = Math.max(rawDelay, MIN_SCHEDULE_INTERVAL_MS);
|
|
649
933
|
if (delay > MAX_TIMER_DELAY_MS) {
|
|
650
934
|
const hop = setTimeout(() => {
|
|
651
|
-
if (this.started && job
|
|
935
|
+
if (this.started && this.shouldArm(job)) this.scheduleNext(id);
|
|
652
936
|
}, MAX_TIMER_DELAY_MS);
|
|
653
937
|
if (hop && typeof hop === "object" && "unref" in hop) hop.unref();
|
|
654
938
|
job.timerId = hop;
|
|
655
939
|
return;
|
|
656
940
|
}
|
|
657
941
|
const timer = setTimeout(async () => {
|
|
658
|
-
if (!
|
|
659
|
-
if (
|
|
660
|
-
logger.warn(`[cron] Skipping scheduled run of "${id}" — still executing from previous run`);
|
|
661
|
-
this.recordSkip(job, "already_executing", false);
|
|
942
|
+
if (!this.started) return;
|
|
943
|
+
if (Date.now() < nextRun.getTime()) {
|
|
662
944
|
this.scheduleNext(id);
|
|
663
945
|
return;
|
|
664
946
|
}
|
|
665
|
-
if (
|
|
947
|
+
if (!await this.isEnabledNow(job)) {
|
|
948
|
+
if (this.started && this.shouldArm(job)) this.scheduleNext(id);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (!this.started) return;
|
|
952
|
+
if (job.executing) {
|
|
953
|
+
logger.warn(`[cron] Skipping scheduled run of "${id}" — still executing from previous run`);
|
|
954
|
+
this.recordSkip(job, "already_executing", false);
|
|
666
955
|
this.scheduleNext(id);
|
|
667
956
|
return;
|
|
668
957
|
}
|
|
@@ -675,12 +964,12 @@ var CronScheduler = class {
|
|
|
675
964
|
}
|
|
676
965
|
if (!claimed) {
|
|
677
966
|
logger.info(`[cron] Slot ${nextRun.toISOString()} for "${id}" claimed by another instance — skipping`);
|
|
678
|
-
if (this.started && job
|
|
967
|
+
if (this.started && this.shouldArm(job)) this.scheduleNext(id);
|
|
679
968
|
return;
|
|
680
969
|
}
|
|
681
970
|
}
|
|
682
971
|
await this.executeJob(job, false);
|
|
683
|
-
if (this.started && job
|
|
972
|
+
if (this.started && this.shouldArm(job)) this.scheduleNext(id);
|
|
684
973
|
}, delay);
|
|
685
974
|
if (timer && typeof timer === "object" && "unref" in timer) timer.unref();
|
|
686
975
|
job.timerId = timer;
|
|
@@ -707,15 +996,17 @@ var CronScheduler = class {
|
|
|
707
996
|
* otherwise ticking correctly.
|
|
708
997
|
*/
|
|
709
998
|
async catchUpMissedSlots() {
|
|
710
|
-
const candidates = [...this.jobs.values()].filter((job) => job
|
|
999
|
+
const candidates = [...this.jobs.values()].filter((job) => this.shouldArm(job) && (job.definition.catchUpWindowSeconds ?? 0) > 0);
|
|
711
1000
|
if (candidates.length === 0) return;
|
|
712
1001
|
if (!this.store?.tryClaimRun) {
|
|
713
1002
|
logger.warn(`[cron] Catch-up is configured on ${candidates.length} job(s) but no claims-capable store is attached — skipping. Without claims a restart cannot tell an unrun slot from one the previous instance already ran.`);
|
|
714
1003
|
return;
|
|
715
1004
|
}
|
|
716
|
-
const now = /* @__PURE__ */ new Date();
|
|
717
1005
|
for (const job of candidates) try {
|
|
718
|
-
if (!this.started ||
|
|
1006
|
+
if (!this.started || job.executing) continue;
|
|
1007
|
+
if (!await this.isEnabledNow(job)) continue;
|
|
1008
|
+
if (!this.started || job.executing) continue;
|
|
1009
|
+
const now = /* @__PURE__ */ new Date();
|
|
719
1010
|
const windowSeconds = job.definition.catchUpWindowSeconds;
|
|
720
1011
|
const from = /* @__PURE__ */ new Date(now.getTime() - windowSeconds * 1e3);
|
|
721
1012
|
const slot = findMostRecentSlot(job.definition.schedule, from, now, job.definition.timezone);
|
|
@@ -756,11 +1047,34 @@ var CronScheduler = class {
|
|
|
756
1047
|
* - Persists to store (non-blocking) if available
|
|
757
1048
|
* - Always restores state even on catastrophic errors
|
|
758
1049
|
*/
|
|
759
|
-
|
|
1050
|
+
executeJob(job, manual, seedLog) {
|
|
1051
|
+
const abort = new AbortController();
|
|
1052
|
+
this.runControllers.add(abort);
|
|
1053
|
+
const run = this.runJob(job, manual, abort, seedLog);
|
|
1054
|
+
this.track(run);
|
|
1055
|
+
return run.finally(() => {
|
|
1056
|
+
this.runControllers.delete(abort);
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
/** Hold `stop()` until `work` settles. */
|
|
1060
|
+
track(work) {
|
|
1061
|
+
this.inFlight.add(work);
|
|
1062
|
+
const release = () => {
|
|
1063
|
+
this.inFlight.delete(work);
|
|
1064
|
+
};
|
|
1065
|
+
work.then(release, release);
|
|
1066
|
+
}
|
|
1067
|
+
async runJob(job, manual, abort, seedLog) {
|
|
1068
|
+
if (job.executing) return this.recordSkip(job, "already_executing", manual);
|
|
1069
|
+
job.executing = true;
|
|
1070
|
+
const lease = await this.acquireLease(job);
|
|
1071
|
+
if (!lease.acquired) {
|
|
1072
|
+
job.executing = false;
|
|
1073
|
+
logger.warn(`[cron] Skipping ${manual ? "manual trigger" : "scheduled run"} of "${job.id}" — it is running on ${lease.holder ?? "another process"}`);
|
|
1074
|
+
return this.recordSkip(job, "already_executing", manual, lease.holder ?? "another process");
|
|
1075
|
+
}
|
|
760
1076
|
const startedAt = /* @__PURE__ */ new Date();
|
|
761
1077
|
const capturedLogs = seedLog ? [seedLog] : [];
|
|
762
|
-
job.executing = true;
|
|
763
|
-
const abort = new AbortController();
|
|
764
1078
|
const ctx = {
|
|
765
1079
|
jobId: job.id,
|
|
766
1080
|
scheduledAt: startedAt,
|
|
@@ -777,26 +1091,26 @@ var CronScheduler = class {
|
|
|
777
1091
|
let success = true;
|
|
778
1092
|
let error;
|
|
779
1093
|
let result;
|
|
1094
|
+
const aborted = new Promise((_, reject) => {
|
|
1095
|
+
const end = () => reject(abort.signal.reason);
|
|
1096
|
+
if (abort.signal.aborted) end();
|
|
1097
|
+
else abort.signal.addEventListener("abort", end, { once: true });
|
|
1098
|
+
});
|
|
1099
|
+
aborted.catch(() => void 0);
|
|
780
1100
|
try {
|
|
781
|
-
const timeout = (job.definition.timeoutSeconds ??
|
|
782
|
-
const
|
|
783
|
-
let timeoutHandle;
|
|
784
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
785
|
-
timeoutHandle = setTimeout(() => {
|
|
786
|
-
abort.abort(/* @__PURE__ */ new Error(`Cron job "${job.id}" timed out after ${timeout}ms`));
|
|
787
|
-
reject(/* @__PURE__ */ new Error(`Cron job "${job.id}" timed out after ${timeout}ms`));
|
|
788
|
-
}, timeout);
|
|
789
|
-
});
|
|
1101
|
+
const timeout = (job.definition.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3;
|
|
1102
|
+
const cancelTimeout = Number.isFinite(timeout) ? setLongTimeout(() => abort.abort(/* @__PURE__ */ new Error(`Cron job "${job.id}" timed out after ${timeout}ms`)), timeout) : void 0;
|
|
790
1103
|
try {
|
|
791
|
-
result = await Promise.race([
|
|
1104
|
+
result = await Promise.race([Promise.resolve(job.definition.handler(ctx)), aborted]);
|
|
792
1105
|
} finally {
|
|
793
|
-
|
|
1106
|
+
cancelTimeout?.();
|
|
794
1107
|
}
|
|
795
1108
|
} catch (err) {
|
|
796
1109
|
success = false;
|
|
797
1110
|
error = redactSensitiveText(err instanceof Error ? err.message : String(err));
|
|
798
1111
|
job.totalFailures++;
|
|
799
1112
|
} finally {
|
|
1113
|
+
if (lease.token) await this.releaseLease(job, lease.token);
|
|
800
1114
|
job.executing = false;
|
|
801
1115
|
}
|
|
802
1116
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
@@ -817,31 +1131,43 @@ var CronScheduler = class {
|
|
|
817
1131
|
};
|
|
818
1132
|
job.logs.push(logEntry);
|
|
819
1133
|
if (job.logs.length > MAX_LOGS_PER_JOB) job.logs.shift();
|
|
820
|
-
if (this.store) this.store.insertLog(logEntry).catch((persistErr) => {
|
|
1134
|
+
if (this.store) this.track(this.store.insertLog(logEntry).catch((persistErr) => {
|
|
821
1135
|
logger.error(`[cron] Failed to persist log for "${job.id}"`, { error: persistErr });
|
|
822
|
-
});
|
|
1136
|
+
}));
|
|
823
1137
|
if (success) logger.info(`✅ [cron] "${job.id}" completed in ${durationMs}ms`);
|
|
824
1138
|
else logger.error(`❌ [cron] "${job.id}" failed in ${durationMs}ms: ${error}`);
|
|
825
1139
|
return logEntry;
|
|
826
1140
|
}
|
|
827
|
-
|
|
1141
|
+
/**
|
|
1142
|
+
* A job's status: this process's own record, overlaid with what `view`
|
|
1143
|
+
* read from the store. A paused job shows no next run, even where its
|
|
1144
|
+
* timer stays armed to read the state again at that slot.
|
|
1145
|
+
*/
|
|
1146
|
+
toStatus(job, view = {}) {
|
|
1147
|
+
const summary = view.summary;
|
|
1148
|
+
const nextRunAt = job.enabled ? job.nextRunAt ?? view.nextRunAt : void 0;
|
|
1149
|
+
let state = job.state;
|
|
1150
|
+
if (!job.executing) {
|
|
1151
|
+
if (view.runningElsewhere) state = "running";
|
|
1152
|
+
else if (summary && job.enabled) state = summary.lastSuccess === false ? "error" : "idle";
|
|
1153
|
+
}
|
|
828
1154
|
return {
|
|
829
1155
|
id: job.id,
|
|
830
1156
|
name: job.definition.name ?? job.id,
|
|
831
1157
|
description: job.definition.description,
|
|
832
1158
|
schedule: job.definition.schedule,
|
|
833
1159
|
enabled: job.enabled,
|
|
834
|
-
state
|
|
835
|
-
lastRunAt: job.lastRunAt?.toISOString(),
|
|
836
|
-
nextRunAt:
|
|
837
|
-
lastDurationMs: job.lastDurationMs,
|
|
838
|
-
lastError: job.lastError,
|
|
839
|
-
totalRuns: job.totalRuns,
|
|
840
|
-
totalFailures: job.totalFailures
|
|
1160
|
+
state,
|
|
1161
|
+
lastRunAt: summary ? summary.lastRunAt : job.lastRunAt?.toISOString(),
|
|
1162
|
+
nextRunAt: nextRunAt?.toISOString(),
|
|
1163
|
+
lastDurationMs: summary ? summary.lastDurationMs : job.lastDurationMs,
|
|
1164
|
+
lastError: summary ? summary.lastSuccess === false ? summary.lastError : void 0 : job.lastError,
|
|
1165
|
+
totalRuns: summary ? summary.totalRuns : job.totalRuns,
|
|
1166
|
+
totalFailures: summary ? summary.totalFailures : job.totalFailures
|
|
841
1167
|
};
|
|
842
1168
|
}
|
|
843
1169
|
};
|
|
844
1170
|
//#endregion
|
|
845
|
-
export { parseEnvBoolean as i, cron_scheduler_exports as n,
|
|
1171
|
+
export { parseEnvBoolean as a, validateCronExpression as i, cron_scheduler_exports as n, isAlreadyExecutingSkip as r, CronScheduler as t };
|
|
846
1172
|
|
|
847
|
-
//# sourceMappingURL=cron-scheduler-
|
|
1173
|
+
//# sourceMappingURL=cron-scheduler-B0pLfAix.js.map
|