@rebasepro/server 0.22.0 → 0.24.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/README.md +1 -1
- package/bin/rebase-server.js +4 -2
- package/dist/{GCSStorageController-CLIJXwGS.js → GCSStorageController-BSiP1c-f.js} +57 -29
- package/dist/GCSStorageController-BSiP1c-f.js.map +1 -0
- package/dist/{S3StorageController-Dcuf8lMA.js → S3StorageController-CAwFRgjV.js} +19 -7
- package/dist/S3StorageController-CAwFRgjV.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +127 -1
- package/dist/api/errors.d.ts +9 -0
- package/dist/api/live-schema-routes.d.ts +52 -8
- package/dist/api/logs-routes.d.ts +39 -1
- package/dist/api/openapi-generator.d.ts +17 -0
- package/dist/api/rest/api-generator.d.ts +158 -42
- 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 +34 -2
- package/dist/api/types.d.ts +17 -1
- package/dist/{ast-schema-editor-CslO8Oje.js → ast-schema-editor-CWqS_sLJ.js} +411 -13
- package/dist/ast-schema-editor-CWqS_sLJ.js.map +1 -0
- package/dist/auth/access.d.ts +105 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/address-ownership.d.ts +68 -0
- package/dist/auth/admin-roles-route.d.ts +4 -2
- package/dist/auth/admin-roles.d.ts +17 -20
- package/dist/auth/admin-user-ops.d.ts +35 -2
- package/dist/auth/admin-users-route.d.ts +1 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
- package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
- package/dist/auth/api-keys/api-key-store.d.ts +31 -8
- package/dist/auth/api-keys/api-key-types.d.ts +14 -16
- package/dist/auth/api-keys/http-operation.d.ts +19 -0
- package/dist/auth/api-keys/index.d.ts +11 -11
- package/dist/auth/api-keys/key-grant.d.ts +41 -0
- package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
- package/dist/auth/auth-hooks.d.ts +50 -7
- package/dist/auth/builtin-auth-adapter.d.ts +8 -0
- package/dist/auth/captcha.d.ts +5 -0
- package/dist/auth/cookie-utils.d.ts +7 -0
- package/dist/auth/deliverable-address.d.ts +6 -0
- package/dist/auth/email-change-routes.d.ts +41 -0
- package/dist/auth/expired-token-sweep.d.ts +67 -0
- package/dist/auth/impersonation.d.ts +110 -0
- package/dist/auth/index.d.ts +4 -2
- package/dist/auth/interfaces.d.ts +146 -65
- package/dist/auth/jwt.d.ts +66 -3
- package/dist/auth/magic-link-routes.d.ts +2 -6
- package/dist/auth/mfa-routes.d.ts +2 -9
- package/dist/auth/middleware.d.ts +17 -5
- package/dist/auth/oauth-signin-policy.d.ts +25 -8
- package/dist/auth/otp-routes.d.ts +2 -6
- package/dist/auth/passwordless-signup.d.ts +27 -0
- package/dist/auth/platform-token.d.ts +122 -0
- package/dist/auth/rate-limiter.d.ts +72 -1
- package/dist/auth/routes.d.ts +45 -0
- package/dist/auth/scope-routes.d.ts +22 -0
- package/dist/auth/session-routes.d.ts +18 -6
- package/dist/auth/token-revocation.d.ts +53 -1
- package/dist/auth/verify-credential.d.ts +28 -0
- package/dist/{auth-CCDpk2rn.js → auth-DMLngxn_.js} +2712 -711
- package/dist/auth-DMLngxn_.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 +29 -0
- package/dist/backup/backup-routes.d.ts +24 -4
- package/dist/backup/backup-schedule.d.ts +33 -0
- package/dist/backup/backup-storage.d.ts +14 -0
- package/dist/backup/index.d.ts +2 -0
- package/dist/backup-CN0s50D2.js +444 -0
- package/dist/backup-CN0s50D2.js.map +1 -0
- package/dist/boot/bundle.d.ts +19 -0
- package/dist/boot/driver.d.ts +10 -0
- package/dist/boot/env.d.ts +51 -6
- package/dist/boot/fetch-bundle.d.ts +18 -1
- package/dist/boot/rls-audit-option.d.ts +26 -0
- package/dist/boot/security-headers.d.ts +26 -0
- package/dist/boot/sources.d.ts +1 -0
- package/dist/boot/static-routing.d.ts +56 -0
- package/dist/collection_patch-BRu-BvDv.js +472 -0
- package/dist/collection_patch-BRu-BvDv.js.map +1 -0
- package/dist/{contract-routes-eLxV0le1.js → contract-routes-fz8i4pxs.js} +17 -4
- package/dist/contract-routes-fz8i4pxs.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +7 -2
- package/dist/cron/cron-scheduler.d.ts +146 -21
- package/dist/cron/cron-store.d.ts +76 -8
- package/dist/cron/index.d.ts +1 -1
- package/dist/{cron-loader-CQjvjpEw.js → cron-loader-CwaANlOG.js} +4 -4
- package/dist/cron-loader-CwaANlOG.js.map +1 -0
- package/dist/cron-routes-Bc-SB0Se.js +96 -0
- package/dist/cron-routes-Bc-SB0Se.js.map +1 -0
- package/dist/{cron-scheduler-COPQxlEq.js → cron-scheduler-CYQgco86.js} +427 -83
- package/dist/cron-scheduler-CYQgco86.js.map +1 -0
- package/dist/{cron-store-BYGZFNWk.js → cron-store-D2Q9-Aco.js} +139 -23
- package/dist/cron-store-D2Q9-Aco.js.map +1 -0
- package/dist/{ddl-bootstrap-CfNvxMuK.js → ddl-bootstrap-BaqMSa4Y.js} +3 -26
- package/dist/ddl-bootstrap-BaqMSa4Y.js.map +1 -0
- package/dist/email/index.d.ts +2 -2
- 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 +31 -0
- package/dist/email/types.d.ts +29 -2
- package/dist/env.d.ts +25 -7
- package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
- package/dist/errors-D6_y86c5.js.map +1 -0
- package/dist/{function-loader-xnbDAPfa.js → function-loader-D7o5Epjj.js} +2 -2
- package/dist/{function-loader-xnbDAPfa.js.map → function-loader-D7o5Epjj.js.map} +1 -1
- package/dist/{function-routes-Chet4-lB.js → function-routes-CaNG4waN.js} +24 -12
- package/dist/function-routes-CaNG4waN.js.map +1 -0
- package/dist/functions/context.d.ts +17 -6
- package/dist/functions/guards.d.ts +22 -5
- package/dist/functions/index.d.ts +2 -2
- package/dist/functions/index.js +90 -36
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-BQmB0P_j.js → history-recorder-Nr8zLvoU.js} +9 -7
- package/dist/history-recorder-Nr8zLvoU.js.map +1 -0
- package/dist/{history-store-CetkrBBD.js → history-store-rcAm_xFR.js} +2 -2
- package/dist/{history-store-CetkrBBD.js.map → history-store-rcAm_xFR.js.map} +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.es.js +5733 -1551
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +5 -2
- package/dist/init/health.d.ts +17 -2
- package/dist/init/shutdown.d.ts +18 -3
- package/dist/init.d.ts +54 -0
- 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-DqYNfquG.js} +306 -167
- package/dist/jobs-DqYNfquG.js.map +1 -0
- package/dist/{jwt-C4OW-DNq.js → jwt-R6bSPMjk.js} +114 -38
- package/dist/{jwt-C4OW-DNq.js.map → jwt-R6bSPMjk.js.map} +1 -1
- package/dist/{keys-Qfc4XieN.js → keys-GAVZqbqx.js} +18 -17
- package/dist/{keys-Qfc4XieN.js.map → keys-GAVZqbqx.js.map} +1 -1
- package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
- package/dist/logger-D-S-hO5e.js.map +1 -0
- package/dist/{logs-routes-3EEzPjhl.js → logs-routes-DAdv37GI.js} +54 -11
- package/dist/logs-routes-DAdv37GI.js.map +1 -0
- package/dist/mcp/consent-page.d.ts +1 -1
- package/dist/mcp/mcp-routes.d.ts +45 -2
- package/dist/mcp/mcp-tools.d.ts +22 -10
- package/dist/mcp/oauth-metadata.d.ts +21 -16
- package/dist/mcp/oauth-routes.d.ts +34 -1
- 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-DAq_XVDu.js} +135 -22
- package/dist/openapi-generator-DAq_XVDu.js.map +1 -0
- package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
- package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
- package/dist/{query-parser-BleZmY18.js → query-parser-BgiKJKvc.js} +41 -82
- package/dist/query-parser-BgiKJKvc.js.map +1 -0
- package/dist/{request-timeout-C_4C2BeR.js → request-timeout-DgH7j8qO.js} +3 -3
- package/dist/{request-timeout-C_4C2BeR.js.map → request-timeout-DgH7j8qO.js.map} +1 -1
- package/dist/rls-audit/index.d.ts +4 -0
- package/dist/schema-edit/apply-schema-change.d.ts +63 -3
- package/dist/schema-edit/project-root.d.ts +3 -2
- package/dist/schema-edit/remote-source.d.ts +9 -4
- package/dist/{schema-editor-routes-DdLihzp0.js → schema-editor-routes-oIyuWl3L.js} +12 -7
- package/dist/schema-editor-routes-oIyuWl3L.js.map +1 -0
- package/dist/serve-spa.d.ts +58 -0
- package/dist/services/routed-realtime-service.d.ts +11 -0
- package/dist/soft-delete-params-BWPilMPF.js +59 -0
- package/dist/soft-delete-params-BWPilMPF.js.map +1 -0
- package/dist/{src-Br6ARbs6.js → src-CatHFUym.js} +439 -45
- package/dist/src-CatHFUym.js.map +1 -0
- package/dist/{src-1vL-I1Po.js → src-I3aG1PcY.js} +371 -81
- package/dist/src-I3aG1PcY.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +13 -1
- package/dist/storage/LocalStorageController.d.ts +2 -0
- package/dist/storage/S3StorageController.d.ts +2 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/keys.d.ts +12 -0
- package/dist/storage/property-limits.d.ts +41 -6
- package/dist/storage/rendition-cache.d.ts +11 -1
- package/dist/storage/request-keys.d.ts +82 -0
- package/dist/storage/requested-object.d.ts +74 -0
- package/dist/storage/routes.d.ts +36 -18
- package/dist/storage/tus-handler.d.ts +30 -5
- package/dist/storage/types.d.ts +36 -1
- package/dist/types-BfKcm9do.js.map +1 -1
- package/dist/utils/logger.d.ts +12 -0
- package/package.json +9 -9
- package/dist/GCSStorageController-CLIJXwGS.js.map +0 -1
- package/dist/S3StorageController-Dcuf8lMA.js.map +0 -1
- package/dist/admin-roles-vYdp_Pil.js +0 -36
- package/dist/admin-roles-vYdp_Pil.js.map +0 -1
- package/dist/admin_block-DxKLmdiv.js +0 -206
- package/dist/admin_block-DxKLmdiv.js.map +0 -1
- package/dist/ast-schema-editor-CslO8Oje.js.map +0 -1
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
- package/dist/auth-CCDpk2rn.js.map +0 -1
- package/dist/backup-DzI9jLwc.js +0 -192
- package/dist/backup-DzI9jLwc.js.map +0 -1
- package/dist/contract-routes-eLxV0le1.js.map +0 -1
- package/dist/cron-loader-CQjvjpEw.js.map +0 -1
- package/dist/cron-routes-B7CRGfiq.js +0 -72
- 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/errors-DWsX4yTd.js.map +0 -1
- package/dist/function-routes-Chet4-lB.js.map +0 -1
- package/dist/history-recorder-BQmB0P_j.js.map +0 -1
- package/dist/jobs-Bjr8DZAi.js.map +0 -1
- package/dist/logger-DO2PZc4i.js.map +0 -1
- package/dist/logs-routes-3EEzPjhl.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/schema-editor-routes-DdLihzp0.js.map +0 -1
- package/dist/src-1vL-I1Po.js.map +0 -1
- package/dist/src-Br6ARbs6.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-
|
|
7
|
-
import { a as redactSensitiveText, r as logger } from "./logger-
|
|
6
|
+
import "./src-CatHFUym.js";
|
|
7
|
+
import { a as redactSensitiveText, r as logger, s as stringifyForLog } from "./logger-D-S-hO5e.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
|
|
@@ -426,23 +502,19 @@ var CronScheduler = class {
|
|
|
426
502
|
const validation = validateCronExpression(loaded.definition.schedule);
|
|
427
503
|
if (!validation.valid) {
|
|
428
504
|
logger.error(`[cron] Rejecting job "${loaded.id}": invalid schedule "${loaded.definition.schedule}" — ${validation.reason}`);
|
|
429
|
-
this.
|
|
430
|
-
id: loaded.id,
|
|
431
|
-
name: loaded.definition.name ?? loaded.id,
|
|
432
|
-
schedule: loaded.definition.schedule,
|
|
433
|
-
reason: validation.reason
|
|
434
|
-
});
|
|
505
|
+
this.refuse(loaded, validation.reason);
|
|
435
506
|
continue;
|
|
436
507
|
}
|
|
437
508
|
if (loaded.definition.timezone !== void 0 && !isValidTimeZone(loaded.definition.timezone)) {
|
|
438
509
|
const reason = `unknown timezone "${loaded.definition.timezone}" — use an IANA name such as "Europe/Madrid" or "UTC"`;
|
|
439
510
|
logger.error(`[cron] Rejecting job "${loaded.id}": ${reason}.`);
|
|
440
|
-
this.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
});
|
|
511
|
+
this.refuse(loaded, reason);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const timeoutProblem = invalidTimeoutReason(loaded.definition.timeoutSeconds);
|
|
515
|
+
if (timeoutProblem) {
|
|
516
|
+
logger.error(`[cron] Rejecting job "${loaded.id}": ${timeoutProblem}.`);
|
|
517
|
+
this.refuse(loaded, timeoutProblem);
|
|
446
518
|
continue;
|
|
447
519
|
}
|
|
448
520
|
this.rejected.delete(loaded.id);
|
|
@@ -451,7 +523,7 @@ var CronScheduler = class {
|
|
|
451
523
|
this.stopJob(loaded.id);
|
|
452
524
|
}
|
|
453
525
|
const enabled = loaded.definition.enabled !== false;
|
|
454
|
-
|
|
526
|
+
const job = {
|
|
455
527
|
id: loaded.id,
|
|
456
528
|
definition: loaded.definition,
|
|
457
529
|
enabled,
|
|
@@ -460,8 +532,9 @@ var CronScheduler = class {
|
|
|
460
532
|
totalFailures: 0,
|
|
461
533
|
logs: [],
|
|
462
534
|
executing: false
|
|
463
|
-
}
|
|
464
|
-
|
|
535
|
+
};
|
|
536
|
+
this.jobs.set(loaded.id, job);
|
|
537
|
+
if (this.started && this.shouldArm(job)) this.scheduleNext(loaded.id);
|
|
465
538
|
}
|
|
466
539
|
}
|
|
467
540
|
/**
|
|
@@ -482,21 +555,42 @@ var CronScheduler = class {
|
|
|
482
555
|
}).catch((err) => {
|
|
483
556
|
logger.warn("[cron] Failed to seed job stats from database", { error: err });
|
|
484
557
|
});
|
|
485
|
-
for (const [id, job] of this.jobs) if (job
|
|
558
|
+
for (const [id, job] of this.jobs) if (this.shouldArm(job)) this.scheduleNext(id);
|
|
486
559
|
if (!this.store) logger.warn("[cron] No cron store attached — runs are uncoordinated; with multiple app instances every instance will execute every job");
|
|
487
560
|
this.warnIfScaleToZero();
|
|
488
561
|
this.catchUpMissedSlots();
|
|
489
562
|
logger.info(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);
|
|
490
563
|
}
|
|
491
564
|
/**
|
|
492
|
-
* Stop the scheduler
|
|
565
|
+
* Stop the scheduler: clear every timer, then wait for the runs executing
|
|
566
|
+
* right now — for at most `timeoutMs` when given.
|
|
567
|
+
*
|
|
568
|
+
* A run still going when the budget runs out has its `ctx.signal` aborted
|
|
569
|
+
* and ends there, recorded as a failure that says why. Its slot stays
|
|
570
|
+
* claimed, so no other instance re-runs it; a handler that must not be cut
|
|
571
|
+
* short should watch its signal and leave its work resumable.
|
|
493
572
|
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
573
|
+
* Nothing is scheduled after `stop()`, whether or not the returned promise
|
|
574
|
+
* is awaited: the timers are cleared before it first yields.
|
|
496
575
|
*/
|
|
497
|
-
stop() {
|
|
576
|
+
async stop(timeoutMs) {
|
|
498
577
|
this.started = false;
|
|
499
578
|
for (const [id] of this.jobs) this.stopJob(id);
|
|
579
|
+
if (this.inFlight.size === 0) return;
|
|
580
|
+
const settled = Promise.allSettled([...this.inFlight]).then(() => true);
|
|
581
|
+
if (timeoutMs === void 0) {
|
|
582
|
+
await settled;
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
let budget;
|
|
586
|
+
const finished = await Promise.race([settled, new Promise((resolve) => {
|
|
587
|
+
budget = setTimeout(() => resolve(false), timeoutMs);
|
|
588
|
+
if (budget && typeof budget === "object" && "unref" in budget) budget.unref();
|
|
589
|
+
})]);
|
|
590
|
+
clearTimeout(budget);
|
|
591
|
+
if (finished || this.runControllers.size === 0) return;
|
|
592
|
+
logger.warn(`[cron] ${this.runControllers.size} run(s) still executing after ${timeoutMs}ms — aborting them`);
|
|
593
|
+
for (const controller of this.runControllers) controller.abort(/* @__PURE__ */ new Error("The server is shutting down; the run was stopped before it finished"));
|
|
500
594
|
}
|
|
501
595
|
/**
|
|
502
596
|
* List all registered jobs with their current status.
|
|
@@ -504,6 +598,18 @@ var CronScheduler = class {
|
|
|
504
598
|
listJobs() {
|
|
505
599
|
return [...this.jobs.values()].map((job) => this.toStatus(job));
|
|
506
600
|
}
|
|
601
|
+
/** Keep a refused job, so the admin surface can say why it never runs. */
|
|
602
|
+
refuse(loaded, reason) {
|
|
603
|
+
this.rejected.set(loaded.id, {
|
|
604
|
+
job: {
|
|
605
|
+
id: loaded.id,
|
|
606
|
+
name: loaded.definition.name ?? loaded.id,
|
|
607
|
+
schedule: loaded.definition.schedule,
|
|
608
|
+
reason
|
|
609
|
+
},
|
|
610
|
+
definition: loaded.definition
|
|
611
|
+
});
|
|
612
|
+
}
|
|
507
613
|
/**
|
|
508
614
|
* Jobs that loaded but whose schedule the scheduler refused.
|
|
509
615
|
*
|
|
@@ -512,7 +618,22 @@ var CronScheduler = class {
|
|
|
512
618
|
* cron will never fire" look identical from the admin panel.
|
|
513
619
|
*/
|
|
514
620
|
listRejectedJobs() {
|
|
515
|
-
return [...this.rejected.values()];
|
|
621
|
+
return [...this.rejected.values()].map((refused) => refused.job);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* {@link listRejectedJobs}, those whose definition matches — how a surface
|
|
625
|
+
* finds a refused job by what it is, as {@link jobIdsWhere} does for the
|
|
626
|
+
* registered ones.
|
|
627
|
+
*/
|
|
628
|
+
rejectedJobsWhere(predicate) {
|
|
629
|
+
return [...this.rejected.values()].filter((refused) => predicate(refused.definition)).map((refused) => refused.job);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* The ids of the registered jobs whose definition matches — how a surface
|
|
633
|
+
* finds a job by what it is rather than by the file it happens to live in.
|
|
634
|
+
*/
|
|
635
|
+
jobIdsWhere(predicate) {
|
|
636
|
+
return [...this.jobs.values()].filter((job) => predicate(job.definition)).map((job) => job.id);
|
|
516
637
|
}
|
|
517
638
|
/**
|
|
518
639
|
* Get a single job status by ID.
|
|
@@ -522,6 +643,31 @@ var CronScheduler = class {
|
|
|
522
643
|
return job ? this.toStatus(job) : void 0;
|
|
523
644
|
}
|
|
524
645
|
/**
|
|
646
|
+
* {@link listJobs}, as the fleet sees it rather than this process alone.
|
|
647
|
+
*
|
|
648
|
+
* Reads the store: every job's enabled state — a pause another replica
|
|
649
|
+
* made — whether another process holds its run lease, and each job's run
|
|
650
|
+
* count and last run from `cron_logs`. The counters in this process's
|
|
651
|
+
* memory count only the runs it made: the `api` role runs nothing and
|
|
652
|
+
* would say 0 runs for a job the worker has run a thousand times, and one
|
|
653
|
+
* of two scheduling replicas never sees the slots the other one claimed.
|
|
654
|
+
*
|
|
655
|
+
* A store that cannot answer leaves this process's own view, with a
|
|
656
|
+
* warning: a listing is not worth failing.
|
|
657
|
+
*/
|
|
658
|
+
async fetchJobs() {
|
|
659
|
+
const jobs = [...this.jobs.values()];
|
|
660
|
+
const views = await this.readFleet(jobs);
|
|
661
|
+
return jobs.map((job) => this.toStatus(job, views.get(job.id)));
|
|
662
|
+
}
|
|
663
|
+
/** {@link getJob}, as {@link fetchJobs} reads it. */
|
|
664
|
+
async fetchJob(id) {
|
|
665
|
+
const job = this.jobs.get(id);
|
|
666
|
+
if (!job) return void 0;
|
|
667
|
+
const views = await this.readFleet([job]);
|
|
668
|
+
return this.toStatus(job, views.get(id));
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
525
671
|
* Get log entries for a job.
|
|
526
672
|
*/
|
|
527
673
|
getJobLogs(id, limit) {
|
|
@@ -532,7 +678,12 @@ var CronScheduler = class {
|
|
|
532
678
|
}
|
|
533
679
|
/**
|
|
534
680
|
* Get log entries for a job from the database (if store is available).
|
|
535
|
-
* Falls back to in-memory logs if no store is configured
|
|
681
|
+
* Falls back to in-memory logs if no store is configured, or if the store
|
|
682
|
+
* has none — a run whose row is still being written.
|
|
683
|
+
*
|
|
684
|
+
* Throws when the store cannot read the history. This process's memory is
|
|
685
|
+
* not a stand-in for it: it is empty on the `api` role and after a
|
|
686
|
+
* restart, and holds one replica's runs at most.
|
|
536
687
|
*/
|
|
537
688
|
async getJobLogsFromDb(id, limit) {
|
|
538
689
|
if (this.store) {
|
|
@@ -542,27 +693,49 @@ var CronScheduler = class {
|
|
|
542
693
|
return this.getJobLogs(id, limit);
|
|
543
694
|
}
|
|
544
695
|
/**
|
|
545
|
-
* Enable or disable a job
|
|
696
|
+
* Enable or disable a job in this process only. `null` returns it to what
|
|
697
|
+
* its code declares.
|
|
698
|
+
*
|
|
699
|
+
* {@link persistJobEnabled} is what the admin API calls: this alone is
|
|
700
|
+
* forgotten on restart and reaches no other replica.
|
|
546
701
|
*/
|
|
547
702
|
setJobEnabled(id, enabled) {
|
|
548
703
|
const job = this.jobs.get(id);
|
|
549
704
|
if (!job) return void 0;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
} else if (!enabled) {
|
|
555
|
-
this.stopJob(id);
|
|
556
|
-
job.state = "disabled";
|
|
557
|
-
}
|
|
705
|
+
const effective = enabled ?? this.codeEnabled(job);
|
|
706
|
+
this.applyEnabled(job, effective);
|
|
707
|
+
if (effective && this.started) this.scheduleNext(id);
|
|
708
|
+
else if (!this.shouldArm(job)) this.stopJob(id);
|
|
558
709
|
return this.toStatus(job);
|
|
559
710
|
}
|
|
560
711
|
/**
|
|
712
|
+
* Enable or disable a job for every process that runs it, and keep it that
|
|
713
|
+
* way across restarts and redeploys: `true` or `false` overrides the code's
|
|
714
|
+
* `enabled`, `null` hands the decision back to it.
|
|
715
|
+
*
|
|
716
|
+
* The override is written to the store first, and only then applied here,
|
|
717
|
+
* so a write that fails throws and changes nothing — a pause that only the
|
|
718
|
+
* replica serving the request would honour is the failure this exists to
|
|
719
|
+
* end. Every scheduler reads the override when a slot comes due, before it
|
|
720
|
+
* claims the slot.
|
|
721
|
+
*
|
|
722
|
+
* Without a store that keeps the state (a driver with no SQL, persistence
|
|
723
|
+
* switched off, a custom store) this is {@link setJobEnabled}: local, and
|
|
724
|
+
* gone on restart.
|
|
725
|
+
*/
|
|
726
|
+
async persistJobEnabled(id, enabled, updatedBy) {
|
|
727
|
+
if (!this.jobs.has(id)) return void 0;
|
|
728
|
+
if (this.store?.saveEnabledOverride) await this.store.saveEnabledOverride(id, enabled, updatedBy);
|
|
729
|
+
return this.setJobEnabled(id, enabled);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
561
732
|
* Manually trigger a job execution immediately.
|
|
562
733
|
*
|
|
563
734
|
* Returns `undefined` if the job doesn't exist.
|
|
564
|
-
* If the job is currently executing,
|
|
565
|
-
* a `skipped: true`
|
|
735
|
+
* If the job is currently executing — here, or on another process that
|
|
736
|
+
* holds its run lease — returns the log entry with a `skipped: true`
|
|
737
|
+
* result rather than running concurrently; {@link isAlreadyExecutingSkip}
|
|
738
|
+
* recognises it.
|
|
566
739
|
*/
|
|
567
740
|
async triggerJob(id) {
|
|
568
741
|
const job = this.jobs.get(id);
|
|
@@ -587,8 +760,9 @@ var CronScheduler = class {
|
|
|
587
760
|
* `success: true` is deliberate: nothing failed. The `result.skipped` flag
|
|
588
761
|
* and the reason are what distinguishes it, and the Studio panel reads them.
|
|
589
762
|
*/
|
|
590
|
-
recordSkip(job, reason, manual) {
|
|
763
|
+
recordSkip(job, reason, manual, runningOn) {
|
|
591
764
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
765
|
+
const why = reason === "already_executing" ? "the previous run has not finished" : reason;
|
|
592
766
|
const logEntry = {
|
|
593
767
|
jobId: job.id,
|
|
594
768
|
startedAt: now,
|
|
@@ -599,16 +773,143 @@ var CronScheduler = class {
|
|
|
599
773
|
skipped: true,
|
|
600
774
|
reason
|
|
601
775
|
},
|
|
602
|
-
logs: [`Skipped: ${
|
|
776
|
+
logs: [`Skipped: ${why}${runningOn ? ` — it is running on ${runningOn}` : ""}`],
|
|
603
777
|
manual
|
|
604
778
|
};
|
|
605
779
|
job.logs.push(logEntry);
|
|
606
780
|
if (job.logs.length > MAX_LOGS_PER_JOB) job.logs.shift();
|
|
607
|
-
this.store
|
|
781
|
+
if (this.store) this.track(this.store.insertLog(logEntry).catch((persistErr) => {
|
|
608
782
|
logger.error(`[cron] Failed to persist skip for "${job.id}"`, { error: persistErr });
|
|
609
|
-
});
|
|
783
|
+
}));
|
|
610
784
|
return logEntry;
|
|
611
785
|
}
|
|
786
|
+
/** What the job's own definition says, before any override. */
|
|
787
|
+
codeEnabled(job) {
|
|
788
|
+
return job.definition.enabled !== false;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Whether the enabled state lives in the store, where every process reads
|
|
792
|
+
* it, rather than in this process alone.
|
|
793
|
+
*/
|
|
794
|
+
sharesEnabledState() {
|
|
795
|
+
return Boolean(this.store?.fetchJobStates && this.store.saveEnabledOverride);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Whether a job gets a timer here. A job paused in this process's memory
|
|
799
|
+
* needs none — nothing but this process can resume it. A job whose state
|
|
800
|
+
* the store keeps does, paused or not: another replica may resume it, and
|
|
801
|
+
* reading the state when its slot comes due is how this one finds out.
|
|
802
|
+
*/
|
|
803
|
+
shouldArm(job) {
|
|
804
|
+
return job.enabled || this.sharesEnabledState();
|
|
805
|
+
}
|
|
806
|
+
/** Set a job's enabled flag, and its state with it unless a run is in progress. */
|
|
807
|
+
applyEnabled(job, enabled) {
|
|
808
|
+
job.enabled = enabled;
|
|
809
|
+
if (job.executing) return;
|
|
810
|
+
if (!enabled) job.state = "disabled";
|
|
811
|
+
else if (job.state === "disabled") job.state = "idle";
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Whether a job may run now: the store's override when it keeps one, else
|
|
815
|
+
* the code's `enabled`.
|
|
816
|
+
*
|
|
817
|
+
* One query per fire, before the slot is claimed — a paused job does not
|
|
818
|
+
* spend its slot. A store that cannot answer falls back to the code's
|
|
819
|
+
* `enabled`, with a warning: a scheduled run fails open, as it does when
|
|
820
|
+
* the claims table cannot answer, because a broken state table must not
|
|
821
|
+
* silently stop every job.
|
|
822
|
+
*/
|
|
823
|
+
async isEnabledNow(job) {
|
|
824
|
+
if (!this.store?.fetchJobStates || !this.sharesEnabledState()) return job.enabled;
|
|
825
|
+
try {
|
|
826
|
+
const enabled = (await this.store.fetchJobStates([job.id])).get(job.id)?.enabled ?? this.codeEnabled(job);
|
|
827
|
+
this.applyEnabled(job, enabled);
|
|
828
|
+
return enabled;
|
|
829
|
+
} catch (err) {
|
|
830
|
+
const enabled = this.codeEnabled(job);
|
|
831
|
+
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 });
|
|
832
|
+
return enabled;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* How long a run's lease lasts: the run's timeout and a grace, so that a
|
|
837
|
+
* crashed holder frees the job once its run would have been aborted
|
|
838
|
+
* anyway. No timeout gets {@link MAX_UNBOUNDED_LEASE_SECONDS}.
|
|
839
|
+
*/
|
|
840
|
+
leaseSeconds(job) {
|
|
841
|
+
const timeout = job.definition.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
842
|
+
return Number.isFinite(timeout) ? timeout + LEASE_GRACE_SECONDS : MAX_UNBOUNDED_LEASE_SECONDS;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Take the job's run lease, the cross-process half of "already
|
|
846
|
+
* executing". A store without leases, or one that cannot answer, lets the
|
|
847
|
+
* run go ahead unleased — with a warning in the second case — the same way
|
|
848
|
+
* a claim that cannot answer does.
|
|
849
|
+
*/
|
|
850
|
+
async acquireLease(job) {
|
|
851
|
+
if (!this.store?.tryAcquireRunLease) return { acquired: true };
|
|
852
|
+
const token = `${this.instanceLabel}#${randomUUID().slice(0, 8)}`;
|
|
853
|
+
try {
|
|
854
|
+
const lease = await this.store.tryAcquireRunLease(job.id, token, this.leaseSeconds(job));
|
|
855
|
+
return lease.acquired ? {
|
|
856
|
+
acquired: true,
|
|
857
|
+
token
|
|
858
|
+
} : lease;
|
|
859
|
+
} catch (err) {
|
|
860
|
+
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 });
|
|
861
|
+
return { acquired: true };
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/** Release a lease this run took. A release that fails leaves it to lapse. */
|
|
865
|
+
async releaseLease(job, token) {
|
|
866
|
+
try {
|
|
867
|
+
await this.store?.releaseRunLease?.(job.id, token);
|
|
868
|
+
} catch (err) {
|
|
869
|
+
logger.warn(`[cron] Could not release the run lease for "${job.id}" — it lapses on its own within ${this.leaseSeconds(job)}s`, { error: err });
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* What the store says about `jobs` beyond this process — see
|
|
874
|
+
* {@link fetchJobs}. Applies the enabled state it reads, and returns the
|
|
875
|
+
* rest as a view for {@link toStatus}. Never throws.
|
|
876
|
+
*/
|
|
877
|
+
async readFleet(jobs) {
|
|
878
|
+
const views = /* @__PURE__ */ new Map();
|
|
879
|
+
const viewOf = (id) => {
|
|
880
|
+
let view = views.get(id);
|
|
881
|
+
if (!view) {
|
|
882
|
+
view = {};
|
|
883
|
+
views.set(id, view);
|
|
884
|
+
}
|
|
885
|
+
return view;
|
|
886
|
+
};
|
|
887
|
+
const store = this.store;
|
|
888
|
+
const ids = jobs.map((job) => job.id);
|
|
889
|
+
if (store?.fetchJobStates && this.sharesEnabledState() && ids.length > 0) try {
|
|
890
|
+
const states = await store.fetchJobStates(ids);
|
|
891
|
+
for (const job of jobs) {
|
|
892
|
+
const state = states.get(job.id);
|
|
893
|
+
this.applyEnabled(job, state?.enabled ?? this.codeEnabled(job));
|
|
894
|
+
if (state?.runningBy && !job.executing) viewOf(job.id).runningElsewhere = true;
|
|
895
|
+
}
|
|
896
|
+
} catch (err) {
|
|
897
|
+
logger.warn("[cron] Could not read the jobs' shared state — listing what this process knows", { error: err });
|
|
898
|
+
}
|
|
899
|
+
if (!this.started) {
|
|
900
|
+
const now = /* @__PURE__ */ new Date();
|
|
901
|
+
for (const job of jobs) try {
|
|
902
|
+
viewOf(job.id).nextRunAt = parseCronExpression(job.definition.schedule, now, job.definition.timezone);
|
|
903
|
+
} catch {}
|
|
904
|
+
}
|
|
905
|
+
if (store?.fetchRunSummaries && ids.length > 0) try {
|
|
906
|
+
const summaries = await store.fetchRunSummaries(ids);
|
|
907
|
+
for (const [id, summary] of summaries) viewOf(id).summary = summary;
|
|
908
|
+
} catch (err) {
|
|
909
|
+
logger.warn("[cron] Could not read the jobs' run history — listing what this process knows", { error: err });
|
|
910
|
+
}
|
|
911
|
+
return views;
|
|
912
|
+
}
|
|
612
913
|
/**
|
|
613
914
|
* Warn once at start when the process looks like it is running on a
|
|
614
915
|
* platform that freezes or evicts instances between requests, where the
|
|
@@ -638,7 +939,7 @@ var CronScheduler = class {
|
|
|
638
939
|
*/
|
|
639
940
|
scheduleNext(id) {
|
|
640
941
|
const job = this.jobs.get(id);
|
|
641
|
-
if (!job || !
|
|
942
|
+
if (!job || !this.started || !this.shouldArm(job)) return;
|
|
642
943
|
this.stopJob(id);
|
|
643
944
|
try {
|
|
644
945
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -648,21 +949,26 @@ var CronScheduler = class {
|
|
|
648
949
|
const delay = Math.max(rawDelay, MIN_SCHEDULE_INTERVAL_MS);
|
|
649
950
|
if (delay > MAX_TIMER_DELAY_MS) {
|
|
650
951
|
const hop = setTimeout(() => {
|
|
651
|
-
if (this.started && job
|
|
952
|
+
if (this.started && this.shouldArm(job)) this.scheduleNext(id);
|
|
652
953
|
}, MAX_TIMER_DELAY_MS);
|
|
653
954
|
if (hop && typeof hop === "object" && "unref" in hop) hop.unref();
|
|
654
955
|
job.timerId = hop;
|
|
655
956
|
return;
|
|
656
957
|
}
|
|
657
958
|
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);
|
|
959
|
+
if (!this.started) return;
|
|
960
|
+
if (Date.now() < nextRun.getTime()) {
|
|
662
961
|
this.scheduleNext(id);
|
|
663
962
|
return;
|
|
664
963
|
}
|
|
665
|
-
if (
|
|
964
|
+
if (!await this.isEnabledNow(job)) {
|
|
965
|
+
if (this.started && this.shouldArm(job)) this.scheduleNext(id);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (!this.started) return;
|
|
969
|
+
if (job.executing) {
|
|
970
|
+
logger.warn(`[cron] Skipping scheduled run of "${id}" — still executing from previous run`);
|
|
971
|
+
this.recordSkip(job, "already_executing", false);
|
|
666
972
|
this.scheduleNext(id);
|
|
667
973
|
return;
|
|
668
974
|
}
|
|
@@ -675,12 +981,12 @@ var CronScheduler = class {
|
|
|
675
981
|
}
|
|
676
982
|
if (!claimed) {
|
|
677
983
|
logger.info(`[cron] Slot ${nextRun.toISOString()} for "${id}" claimed by another instance — skipping`);
|
|
678
|
-
if (this.started && job
|
|
984
|
+
if (this.started && this.shouldArm(job)) this.scheduleNext(id);
|
|
679
985
|
return;
|
|
680
986
|
}
|
|
681
987
|
}
|
|
682
988
|
await this.executeJob(job, false);
|
|
683
|
-
if (this.started && job
|
|
989
|
+
if (this.started && this.shouldArm(job)) this.scheduleNext(id);
|
|
684
990
|
}, delay);
|
|
685
991
|
if (timer && typeof timer === "object" && "unref" in timer) timer.unref();
|
|
686
992
|
job.timerId = timer;
|
|
@@ -707,15 +1013,17 @@ var CronScheduler = class {
|
|
|
707
1013
|
* otherwise ticking correctly.
|
|
708
1014
|
*/
|
|
709
1015
|
async catchUpMissedSlots() {
|
|
710
|
-
const candidates = [...this.jobs.values()].filter((job) => job
|
|
1016
|
+
const candidates = [...this.jobs.values()].filter((job) => this.shouldArm(job) && (job.definition.catchUpWindowSeconds ?? 0) > 0);
|
|
711
1017
|
if (candidates.length === 0) return;
|
|
712
1018
|
if (!this.store?.tryClaimRun) {
|
|
713
1019
|
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
1020
|
return;
|
|
715
1021
|
}
|
|
716
|
-
const now = /* @__PURE__ */ new Date();
|
|
717
1022
|
for (const job of candidates) try {
|
|
718
|
-
if (!this.started ||
|
|
1023
|
+
if (!this.started || job.executing) continue;
|
|
1024
|
+
if (!await this.isEnabledNow(job)) continue;
|
|
1025
|
+
if (!this.started || job.executing) continue;
|
|
1026
|
+
const now = /* @__PURE__ */ new Date();
|
|
719
1027
|
const windowSeconds = job.definition.catchUpWindowSeconds;
|
|
720
1028
|
const from = /* @__PURE__ */ new Date(now.getTime() - windowSeconds * 1e3);
|
|
721
1029
|
const slot = findMostRecentSlot(job.definition.schedule, from, now, job.definition.timezone);
|
|
@@ -756,11 +1064,34 @@ var CronScheduler = class {
|
|
|
756
1064
|
* - Persists to store (non-blocking) if available
|
|
757
1065
|
* - Always restores state even on catastrophic errors
|
|
758
1066
|
*/
|
|
759
|
-
|
|
1067
|
+
executeJob(job, manual, seedLog) {
|
|
1068
|
+
const abort = new AbortController();
|
|
1069
|
+
this.runControllers.add(abort);
|
|
1070
|
+
const run = this.runJob(job, manual, abort, seedLog);
|
|
1071
|
+
this.track(run);
|
|
1072
|
+
return run.finally(() => {
|
|
1073
|
+
this.runControllers.delete(abort);
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
/** Hold `stop()` until `work` settles. */
|
|
1077
|
+
track(work) {
|
|
1078
|
+
this.inFlight.add(work);
|
|
1079
|
+
const release = () => {
|
|
1080
|
+
this.inFlight.delete(work);
|
|
1081
|
+
};
|
|
1082
|
+
work.then(release, release);
|
|
1083
|
+
}
|
|
1084
|
+
async runJob(job, manual, abort, seedLog) {
|
|
1085
|
+
if (job.executing) return this.recordSkip(job, "already_executing", manual);
|
|
1086
|
+
job.executing = true;
|
|
1087
|
+
const lease = await this.acquireLease(job);
|
|
1088
|
+
if (!lease.acquired) {
|
|
1089
|
+
job.executing = false;
|
|
1090
|
+
logger.warn(`[cron] Skipping ${manual ? "manual trigger" : "scheduled run"} of "${job.id}" — it is running on ${lease.holder ?? "another process"}`);
|
|
1091
|
+
return this.recordSkip(job, "already_executing", manual, lease.holder ?? "another process");
|
|
1092
|
+
}
|
|
760
1093
|
const startedAt = /* @__PURE__ */ new Date();
|
|
761
1094
|
const capturedLogs = seedLog ? [seedLog] : [];
|
|
762
|
-
job.executing = true;
|
|
763
|
-
const abort = new AbortController();
|
|
764
1095
|
const ctx = {
|
|
765
1096
|
jobId: job.id,
|
|
766
1097
|
scheduledAt: startedAt,
|
|
@@ -768,6 +1099,7 @@ var CronScheduler = class {
|
|
|
768
1099
|
log: (...args) => {
|
|
769
1100
|
const line = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
770
1101
|
capturedLogs.push(line);
|
|
1102
|
+
logger.info(args.map((a) => typeof a === "string" ? a : stringifyForLog(a)).join(" "), { cron: job.id });
|
|
771
1103
|
},
|
|
772
1104
|
rebase: this.client
|
|
773
1105
|
};
|
|
@@ -777,26 +1109,26 @@ var CronScheduler = class {
|
|
|
777
1109
|
let success = true;
|
|
778
1110
|
let error;
|
|
779
1111
|
let result;
|
|
1112
|
+
const aborted = new Promise((_, reject) => {
|
|
1113
|
+
const end = () => reject(abort.signal.reason);
|
|
1114
|
+
if (abort.signal.aborted) end();
|
|
1115
|
+
else abort.signal.addEventListener("abort", end, { once: true });
|
|
1116
|
+
});
|
|
1117
|
+
aborted.catch(() => void 0);
|
|
780
1118
|
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
|
-
});
|
|
1119
|
+
const timeout = (job.definition.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3;
|
|
1120
|
+
const cancelTimeout = Number.isFinite(timeout) ? setLongTimeout(() => abort.abort(/* @__PURE__ */ new Error(`Cron job "${job.id}" timed out after ${timeout}ms`)), timeout) : void 0;
|
|
790
1121
|
try {
|
|
791
|
-
result = await Promise.race([
|
|
1122
|
+
result = await Promise.race([Promise.resolve(job.definition.handler(ctx)), aborted]);
|
|
792
1123
|
} finally {
|
|
793
|
-
|
|
1124
|
+
cancelTimeout?.();
|
|
794
1125
|
}
|
|
795
1126
|
} catch (err) {
|
|
796
1127
|
success = false;
|
|
797
1128
|
error = redactSensitiveText(err instanceof Error ? err.message : String(err));
|
|
798
1129
|
job.totalFailures++;
|
|
799
1130
|
} finally {
|
|
1131
|
+
if (lease.token) await this.releaseLease(job, lease.token);
|
|
800
1132
|
job.executing = false;
|
|
801
1133
|
}
|
|
802
1134
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
@@ -817,31 +1149,43 @@ var CronScheduler = class {
|
|
|
817
1149
|
};
|
|
818
1150
|
job.logs.push(logEntry);
|
|
819
1151
|
if (job.logs.length > MAX_LOGS_PER_JOB) job.logs.shift();
|
|
820
|
-
if (this.store) this.store.insertLog(logEntry).catch((persistErr) => {
|
|
1152
|
+
if (this.store) this.track(this.store.insertLog(logEntry).catch((persistErr) => {
|
|
821
1153
|
logger.error(`[cron] Failed to persist log for "${job.id}"`, { error: persistErr });
|
|
822
|
-
});
|
|
1154
|
+
}));
|
|
823
1155
|
if (success) logger.info(`✅ [cron] "${job.id}" completed in ${durationMs}ms`);
|
|
824
1156
|
else logger.error(`❌ [cron] "${job.id}" failed in ${durationMs}ms: ${error}`);
|
|
825
1157
|
return logEntry;
|
|
826
1158
|
}
|
|
827
|
-
|
|
1159
|
+
/**
|
|
1160
|
+
* A job's status: this process's own record, overlaid with what `view`
|
|
1161
|
+
* read from the store. A paused job shows no next run, even where its
|
|
1162
|
+
* timer stays armed to read the state again at that slot.
|
|
1163
|
+
*/
|
|
1164
|
+
toStatus(job, view = {}) {
|
|
1165
|
+
const summary = view.summary;
|
|
1166
|
+
const nextRunAt = job.enabled ? job.nextRunAt ?? view.nextRunAt : void 0;
|
|
1167
|
+
let state = job.state;
|
|
1168
|
+
if (!job.executing) {
|
|
1169
|
+
if (view.runningElsewhere) state = "running";
|
|
1170
|
+
else if (summary && job.enabled) state = summary.lastSuccess === false ? "error" : "idle";
|
|
1171
|
+
}
|
|
828
1172
|
return {
|
|
829
1173
|
id: job.id,
|
|
830
1174
|
name: job.definition.name ?? job.id,
|
|
831
1175
|
description: job.definition.description,
|
|
832
1176
|
schedule: job.definition.schedule,
|
|
833
1177
|
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
|
|
1178
|
+
state,
|
|
1179
|
+
lastRunAt: summary ? summary.lastRunAt : job.lastRunAt?.toISOString(),
|
|
1180
|
+
nextRunAt: nextRunAt?.toISOString(),
|
|
1181
|
+
lastDurationMs: summary ? summary.lastDurationMs : job.lastDurationMs,
|
|
1182
|
+
lastError: summary ? summary.lastSuccess === false ? summary.lastError : void 0 : job.lastError,
|
|
1183
|
+
totalRuns: summary ? summary.totalRuns : job.totalRuns,
|
|
1184
|
+
totalFailures: summary ? summary.totalFailures : job.totalFailures
|
|
841
1185
|
};
|
|
842
1186
|
}
|
|
843
1187
|
};
|
|
844
1188
|
//#endregion
|
|
845
|
-
export { parseEnvBoolean as i, cron_scheduler_exports as n,
|
|
1189
|
+
export { parseEnvBoolean as a, validateCronExpression as i, cron_scheduler_exports as n, isAlreadyExecutingSkip as r, CronScheduler as t };
|
|
846
1190
|
|
|
847
|
-
//# sourceMappingURL=cron-scheduler-
|
|
1191
|
+
//# sourceMappingURL=cron-scheduler-CYQgco86.js.map
|