@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc
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 +27 -2
- package/dist/GCSStorageController-fv6JAZ87.js +402 -0
- package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
- package/dist/S3StorageController-BNg-pXiY.js +480 -0
- package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
- package/dist/api/mount-path.d.ts +30 -0
- package/dist/api/mount.d.ts +6 -13
- package/dist/api/rest/api-generator.d.ts +53 -1
- package/dist/api/rest/auth-collection-writes.d.ts +28 -21
- package/dist/api/rest/field-access-query.d.ts +28 -8
- package/dist/api/rest/field-ops.d.ts +19 -7
- package/dist/api/rest/query-body.d.ts +10 -0
- package/dist/api/rest/write-validation.d.ts +7 -5
- package/dist/api/schema-editor-routes.d.ts +10 -1
- package/dist/api/sql-export-routes.d.ts +8 -0
- package/dist/api/types.d.ts +35 -0
- package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
- package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
- package/dist/auth/access.d.ts +57 -8
- package/dist/auth/admin-user-ops.d.ts +24 -2
- package/dist/auth/admin-users-route.d.ts +1 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
- package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
- package/dist/auth/builtin-auth-adapter.d.ts +13 -0
- package/dist/auth/email-change-routes.d.ts +3 -0
- package/dist/auth/expired-token-sweep.d.ts +5 -1
- package/dist/auth/interfaces.d.ts +10 -0
- package/dist/auth/jwks-routes.d.ts +5 -1
- package/dist/auth/jwt-crypto.d.ts +7 -0
- package/dist/auth/jwt-keys.d.ts +4 -0
- package/dist/auth/jwt.d.ts +282 -166
- package/dist/auth/magic-link-routes.d.ts +3 -0
- package/dist/auth/mfa-crypto.d.ts +9 -3
- package/dist/auth/mfa-gate.d.ts +4 -2
- package/dist/auth/mfa-routes.d.ts +6 -0
- package/dist/auth/middleware.d.ts +9 -2
- package/dist/auth/otp-routes.d.ts +3 -0
- package/dist/auth/rate-limiter.d.ts +61 -34
- package/dist/auth/registration-policy.d.ts +17 -0
- package/dist/auth/require-auth.d.ts +1 -1
- package/dist/auth/routes.d.ts +16 -0
- package/dist/auth/session-routes.d.ts +6 -0
- package/dist/auth/user-picking.d.ts +37 -0
- package/dist/auth/verify-credential.d.ts +25 -4
- package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
- package/dist/auth-GnONzteu.js.map +1 -0
- package/dist/auth_adapter-CIItCTc8.js +21 -0
- package/dist/auth_adapter-CIItCTc8.js.map +1 -0
- package/dist/backend/create-backend.d.ts +281 -0
- package/dist/backend/default-backend.d.ts +35 -0
- package/dist/backend/options.d.ts +912 -0
- package/dist/backend/scope.d.ts +23 -0
- package/dist/backend/services.d.ts +54 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +14 -3
- package/dist/backup/backup-routes.d.ts +23 -2
- package/dist/backup/backup-storage.d.ts +3 -1
- package/dist/backup/index.d.ts +3 -1
- package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
- package/dist/backup-BnvkSLHi.js.map +1 -0
- package/dist/boot/boot.d.ts +2 -2
- package/dist/boot/driver.d.ts +3 -0
- package/dist/boot/env.d.ts +9 -2
- package/dist/boot/http-server.d.ts +22 -0
- package/dist/boot/options.d.ts +16 -1
- package/dist/boot/provision.d.ts +4 -2
- package/dist/boot/runtime-cors.d.ts +17 -0
- package/dist/boot/sources.d.ts +2 -0
- package/dist/boot/static-routing.d.ts +9 -0
- package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
- package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
- package/dist/cron/cron-loader.d.ts +77 -18
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +69 -3
- package/dist/cron/cron-store.d.ts +33 -5
- package/dist/cron/define-cron.d.ts +6 -2
- package/dist/cron/index.d.ts +3 -3
- package/dist/cron-loader-B-u0snqq.js +202 -0
- package/dist/cron-loader-B-u0snqq.js.map +1 -0
- package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
- package/dist/cron-routes-OD6BstUj.js.map +1 -0
- package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
- package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
- package/dist/cron-store-CZ30ZBi-.js +577 -0
- package/dist/cron-store-CZ30ZBi-.js.map +1 -0
- package/dist/email/smtp-email-service.d.ts +23 -0
- package/dist/email/types.d.ts +18 -0
- package/dist/env.d.ts +0 -37
- package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
- package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
- package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
- package/dist/function-loader-iKhOZatS.js.map +1 -0
- package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
- package/dist/function-routes-j9TwpvQe.js.map +1 -0
- package/dist/functions/context.d.ts +9 -0
- package/dist/functions/define-function.d.ts +10 -6
- package/dist/functions/function-loader.d.ts +75 -18
- package/dist/functions/function-routes.d.ts +2 -1
- package/dist/functions/index.d.ts +5 -4
- package/dist/functions/index.js +42 -13
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/internal.d.ts +2 -2
- package/dist/functions/proxy.d.ts +6 -2
- package/dist/functions/request-timeout.d.ts +7 -5
- package/dist/history/history-routes.d.ts +9 -0
- package/dist/history/retention-sweep.d.ts +81 -0
- package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
- package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
- package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
- package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
- package/dist/index.d.ts +23 -10
- package/dist/index.es.js +24423 -20038
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +27 -10
- package/dist/init/process-safety.d.ts +7 -2
- package/dist/init/shutdown.d.ts +18 -3
- package/dist/init.d.ts +49 -865
- package/dist/internal-tables-L2t39RdT.js +136 -0
- package/dist/internal-tables-L2t39RdT.js.map +1 -0
- package/dist/jobs/job-queue.d.ts +8 -0
- package/dist/jobs/job-store.d.ts +24 -0
- package/dist/jobs/types.d.ts +25 -4
- package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
- package/dist/jobs-BCbflZj2.js.map +1 -0
- package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
- package/dist/jwt-BlvuKoR6.js.map +1 -0
- package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
- package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
- package/dist/load-failure-L5hvfDoG.js +80 -0
- package/dist/load-failure-L5hvfDoG.js.map +1 -0
- package/dist/mcp/mcp-routes.d.ts +6 -0
- package/dist/mcp/mcp-tools.d.ts +8 -0
- package/dist/mcp/oauth-routes.d.ts +7 -0
- package/dist/metrics/index.d.ts +18 -0
- package/dist/metrics/pool-wait.d.ts +50 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
- package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
- package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
- package/dist/policy-PdzX-Opd.js +121 -0
- package/dist/policy-PdzX-Opd.js.map +1 -0
- package/dist/project_manifest-R6y_KHms.js +59 -0
- package/dist/project_manifest-R6y_KHms.js.map +1 -0
- package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
- package/dist/proxy-CkTlMGmh.js.map +1 -0
- package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
- package/dist/query-parser-4WkMoA_K.js.map +1 -0
- package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
- package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
- package/dist/schema-edit/apply-schema-change.d.ts +10 -0
- package/dist/schema-edit/local-source.d.ts +29 -0
- package/dist/schema-edit/remote-source.d.ts +13 -3
- package/dist/schema-edit/source-editor.d.ts +28 -0
- package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
- package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
- package/dist/services/outbound-post.d.ts +50 -0
- package/dist/services/outbound-url-guard.d.ts +34 -5
- package/dist/services/routed-realtime-service.d.ts +5 -0
- package/dist/services/webhook-service.d.ts +66 -14
- package/dist/singleton.d.ts +9 -5
- package/dist/soft-delete-params-DfV3vVN5.js +668 -0
- package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
- package/dist/sql-export-routes-ClRwzIsC.js +240 -0
- package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
- package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
- package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +46 -2
- package/dist/storage/LocalStorageController.d.ts +67 -2
- package/dist/storage/S3StorageController.d.ts +37 -3
- package/dist/storage/image-transform.d.ts +5 -0
- package/dist/storage/maintenance-routes.d.ts +23 -0
- package/dist/storage/maintenance.d.ts +126 -0
- package/dist/storage/rendition-cache.d.ts +9 -11
- package/dist/storage/request-principals.d.ts +30 -0
- package/dist/storage/routes.d.ts +20 -0
- package/dist/storage/source-variables.d.ts +10 -0
- package/dist/storage/streams.d.ts +42 -0
- package/dist/storage/tus-handler.d.ts +46 -2
- package/dist/storage/types.d.ts +82 -7
- package/dist/storage/upload-validation.d.ts +13 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
- package/dist/strings.es-BeZWAodn.js +256 -0
- package/dist/strings.es-BeZWAodn.js.map +1 -0
- package/dist/upload-validation-Dd-fnsbA.js +172 -0
- package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
- package/dist/utils/byte-size.d.ts +10 -0
- package/dist/utils/compression.d.ts +6 -0
- package/dist/utils/load-failure.d.ts +14 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-BSiP1c-f.js +0 -256
- package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
- package/dist/S3StorageController-CAwFRgjV.js +0 -276
- package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
- package/dist/auth-DMLngxn_.js.map +0 -1
- package/dist/backup-CN0s50D2.js.map +0 -1
- package/dist/contract-routes-fz8i4pxs.js.map +0 -1
- package/dist/cron-loader-CwaANlOG.js +0 -118
- package/dist/cron-loader-CwaANlOG.js.map +0 -1
- package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
- package/dist/cron-scheduler-CYQgco86.js.map +0 -1
- package/dist/cron-store-D2Q9-Aco.js +0 -300
- package/dist/cron-store-D2Q9-Aco.js.map +0 -1
- package/dist/dynamic-import-X40pTZUQ.js +0 -23
- package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
- package/dist/function-loader-D7o5Epjj.js.map +0 -1
- package/dist/function-routes-CaNG4waN.js.map +0 -1
- package/dist/jobs-DqYNfquG.js.map +0 -1
- package/dist/jwt-R6bSPMjk.js.map +0 -1
- package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
- package/dist/proxy-qRlqeUmO.js.map +0 -1
- package/dist/query-parser-BgiKJKvc.js.map +0 -1
- package/dist/request-timeout-DgH7j8qO.js.map +0 -1
- package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
- package/dist/soft-delete-params-BWPilMPF.js +0 -59
- package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
- package/dist/src-I3aG1PcY.js +0 -6870
- package/dist/src-I3aG1PcY.js.map +0 -1
- package/dist/types-BfKcm9do.js +0 -39
- package/dist/types-BfKcm9do.js.map +0 -1
|
@@ -3,10 +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 { r as isAlreadyExecutingSkip } from "./cron-scheduler-
|
|
6
|
+
import { r as isAlreadyExecutingSkip } from "./cron-scheduler-idtLPoN-.js";
|
|
7
7
|
import { r as logger } from "./logger-D-S-hO5e.js";
|
|
8
|
+
import { a as resolveListLimitParam } from "./query-parser-4WkMoA_K.js";
|
|
8
9
|
import { r as errorHandler, t as ApiError } from "./errors-D6_y86c5.js";
|
|
9
|
-
import { a as resolveListLimitParam } from "./query-parser-BgiKJKvc.js";
|
|
10
10
|
import { Hono } from "hono";
|
|
11
11
|
//#region src/cron/cron-routes.ts
|
|
12
12
|
var cron_routes_exports = /* @__PURE__ */ __exportAll({ createCronRoutes: () => createCronRoutes });
|
|
@@ -18,12 +18,24 @@ function actorOf(c) {
|
|
|
18
18
|
return apiKey ? `api-key:${apiKey.id}` : void 0;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
+
* The manual trigger's `?async` switch. Absent means synchronous, as it always
|
|
22
|
+
* was; anything other than a plain yes or no is refused rather than read as
|
|
23
|
+
* one, so `?async=yes-please` does not quietly hold the request open.
|
|
24
|
+
*/
|
|
25
|
+
function readAsyncFlag(raw) {
|
|
26
|
+
if (raw === void 0) return false;
|
|
27
|
+
const value = raw.trim().toLowerCase();
|
|
28
|
+
if (value === "1" || value === "true") return true;
|
|
29
|
+
if (value === "" || value === "0" || value === "false") return false;
|
|
30
|
+
throw ApiError.badRequest(`?async must be 1, true, 0 or false — got "${raw}"`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
21
33
|
* Create admin REST routes for managing cron jobs.
|
|
22
34
|
*
|
|
23
35
|
* Routes:
|
|
24
36
|
* GET / → list all cron jobs
|
|
25
37
|
* GET /:id → get a single job's status
|
|
26
|
-
* POST /:id/trigger → manually trigger a job
|
|
38
|
+
* POST /:id/trigger → manually trigger a job (`?async=1`: 202 once it has started)
|
|
27
39
|
* GET /:id/logs → get execution logs for a job
|
|
28
40
|
* PUT /:id → update job (enable/disable, or `null` to follow the code)
|
|
29
41
|
*
|
|
@@ -56,7 +68,18 @@ function createCronRoutes(scheduler, skipped = 0) {
|
|
|
56
68
|
});
|
|
57
69
|
router.post("/:id/trigger", async (c) => {
|
|
58
70
|
const id = c.req.param("id");
|
|
71
|
+
const asyncRun = readAsyncFlag(c.req.query("async"));
|
|
59
72
|
if (!scheduler.getJob(id)) throw ApiError.notFound(`Cron job "${id}" not found`);
|
|
73
|
+
if (asyncRun) {
|
|
74
|
+
const outcome = await scheduler.startJob(id);
|
|
75
|
+
if (!outcome) throw ApiError.notFound(`Cron job "${id}" not found`);
|
|
76
|
+
if (!outcome.started) throw ApiError.conflict(`Cron job "${id}" is already executing — try again when that run has finished`, "CRON_JOB_ALREADY_EXECUTING", { log: outcome.log });
|
|
77
|
+
return c.json({
|
|
78
|
+
accepted: true,
|
|
79
|
+
startedAt: outcome.startedAt,
|
|
80
|
+
job: await scheduler.fetchJob(id)
|
|
81
|
+
}, 202);
|
|
82
|
+
}
|
|
60
83
|
const log = await scheduler.triggerJob(id);
|
|
61
84
|
if (log && isAlreadyExecutingSkip(log)) throw ApiError.conflict(`Cron job "${id}" is already executing — try again when that run has finished`, "CRON_JOB_ALREADY_EXECUTING", { log });
|
|
62
85
|
return c.json({
|
|
@@ -93,4 +116,4 @@ function createCronRoutes(scheduler, skipped = 0) {
|
|
|
93
116
|
//#endregion
|
|
94
117
|
export { cron_routes_exports as n, createCronRoutes as t };
|
|
95
118
|
|
|
96
|
-
//# sourceMappingURL=cron-routes-
|
|
119
|
+
//# sourceMappingURL=cron-routes-OD6BstUj.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-routes-OD6BstUj.js","names":[],"sources":["../src/cron/cron-routes.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport type { Context } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { isAlreadyExecutingSkip, type CronScheduler } from \"./cron-scheduler\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport { resolveListLimitParam } from \"../api/rest/query-parser\";\nimport { logger } from \"../utils/logger.js\";\n\n/** Who made a change, as `cron_job_state.updated_by` records it. */\nfunction actorOf(c: Context<HonoEnv>): string | undefined {\n const user = c.get(\"user\");\n if (typeof user === \"object\" && user !== null && typeof user.uid === \"string\") return user.uid;\n const apiKey = c.get(\"apiKey\");\n return apiKey ? `api-key:${apiKey.id}` : undefined;\n}\n\n/**\n * The manual trigger's `?async` switch. Absent means synchronous, as it always\n * was; anything other than a plain yes or no is refused rather than read as\n * one, so `?async=yes-please` does not quietly hold the request open.\n */\nfunction readAsyncFlag(raw: string | undefined): boolean {\n if (raw === undefined) return false;\n const value = raw.trim().toLowerCase();\n if (value === \"1\" || value === \"true\") return true;\n if (value === \"\" || value === \"0\" || value === \"false\") return false;\n throw ApiError.badRequest(`?async must be 1, true, 0 or false — got \"${raw}\"`);\n}\n\n/**\n * Create admin REST routes for managing cron jobs.\n *\n * Routes:\n * GET / → list all cron jobs\n * GET /:id → get a single job's status\n * POST /:id/trigger → manually trigger a job (`?async=1`: 202 once it has started)\n * GET /:id/logs → get execution logs for a job\n * PUT /:id → update job (enable/disable, or `null` to follow the code)\n *\n * Every answer about a job's state is read from the store, not from this\n * process's memory: the process serving these routes is often not the one\n * running the jobs (the `api` role never starts its scheduler), and is only\n * ever one replica of several.\n */\nexport function createCronRoutes(scheduler: CronScheduler, skipped = 0): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n // Hono's onError does NOT propagate from parent to child routers, so this\n // child router registers its own handler to format thrown ApiErrors.\n router.onError(errorHandler);\n\n // List all jobs\n router.get(\"/\", async (c) => {\n const jobs = await scheduler.fetchJobs();\n // A file that failed to load is not a job, so it appears nowhere in\n // this list — and \"my job is missing\" and \"my job is not scheduled\"\n // look identical from here. Say how many were dropped, as the\n // functions listing does, so the Studio panel and anyone with curl can\n // see it without boot-log access.\n //\n // Two ways to be dropped, counted together and reported apart. A file\n // the loader could not read has only a count: the failure happened\n // before there was a job to name. A schedule the scheduler refused has a\n // name and a reason — most often \"Expected 5 fields, got 6\", from an\n // expression copied out of a tool that supports seconds — and quoting it\n // here turns a job that silently never fires into a one-line fix.\n const rejected = scheduler.listRejectedJobs();\n const total = skipped + rejected.length;\n return c.json({\n jobs,\n ...(total > 0 && {\n skipped: total,\n ...(rejected.length > 0 && { rejected }),\n note: [\n skipped > 0 ? `${skipped} cron file(s) failed to load` : undefined,\n // Not \"an invalid schedule\": a job is also refused for an\n // unknown timezone or a timeout that is not a positive\n // number, and `rejected` says which.\n rejected.length > 0 ? `${rejected.length} job(s) were refused` : undefined\n ].filter(Boolean).join(\" and \") +\n \" — NOT scheduled. \" +\n (rejected.length > 0\n ? \"See `rejected` for the reason; \"\n : \"\") +\n \"the server log has the rest.\"\n })\n });\n });\n\n // Get single job\n router.get(\"/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const job = await scheduler.fetchJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n return c.json({ job });\n });\n\n // Trigger a job manually\n router.post(\"/:id/trigger\", async (c) => {\n const id = c.req.param(\"id\");\n const asyncRun = readAsyncFlag(c.req.query(\"async\"));\n const job = scheduler.getJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n // `?async=1`: answer once the run has started rather than when it\n // ends. A long job's synchronous answer is cut by any proxy with a\n // shorter timeout than the job (60 s is common) while the run carries\n // on; this one is not. The outcome is in `GET /:id/logs`, as the entry\n // with `manual: true` and this `startedAt`.\n if (asyncRun) {\n const outcome = await scheduler.startJob(id);\n if (!outcome) throw ApiError.notFound(`Cron job \"${id}\" not found`);\n if (!outcome.started) {\n throw ApiError.conflict(\n `Cron job \"${id}\" is already executing — try again when that run has finished`,\n \"CRON_JOB_ALREADY_EXECUTING\",\n { log: outcome.log }\n );\n }\n return c.json({\n accepted: true,\n startedAt: outcome.startedAt,\n job: await scheduler.fetchJob(id)\n }, 202);\n }\n\n const log = await scheduler.triggerJob(id);\n // Nothing ran, so this is not a success: the job is executing already —\n // on this process, or on whichever one holds its run lease (the worker,\n // seen from the api role). The skip is in the run history either way;\n // `details.log` carries it, and its line names the process.\n if (log && isAlreadyExecutingSkip(log)) {\n throw ApiError.conflict(\n `Cron job \"${id}\" is already executing — try again when that run has finished`,\n \"CRON_JOB_ALREADY_EXECUTING\",\n { log }\n );\n }\n return c.json({ log,\njob: await scheduler.fetchJob(id) });\n });\n\n // Get job logs\n router.get(\"/:id/logs\", async (c) => {\n const id = c.req.param(\"id\");\n // Validated, not `parseInt`-ed. `?limit=abc` used to reach the store as\n // `NaN`, where Postgres refused `LIMIT NaN`, the store swallowed the\n // error and returned `[]` — a 200 with an empty list, which reads as\n // \"this job has never run\". The data plane answers 400 for the same\n // input; so does this now.\n const limit = resolveListLimitParam(c.req.query(\"limit\") ?? null, { defaultLimit: 50 });\n\n const job = scheduler.getJob(id);\n if (!job) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n const logs = await scheduler.getJobLogsFromDb(id, limit).catch((err: unknown) => {\n // Not an empty 200: Studio shows that as \"No executions yet\".\n logger.error(`[cron] Could not read the run history of \"${id}\"`, { error: err });\n throw ApiError.serviceUnavailable(\n `The run history of cron job \"${id}\" could not be read. The server log has the reason.`\n );\n });\n return c.json({ logs });\n });\n\n // Enable/disable a job — for every process, and across redeploys\n router.put(\"/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const body: unknown = await c.req.json().catch(() => ({}));\n const enabled = typeof body === \"object\" && body !== null && \"enabled\" in body ? body.enabled : undefined;\n\n if (typeof enabled !== \"boolean\" && enabled !== null) {\n throw ApiError.badRequest(\n \"Missing 'enabled' in body: true or false to override the job's code, or null to follow it again\"\n );\n }\n\n if (!scheduler.getJob(id)) {\n throw ApiError.notFound(`Cron job \"${id}\" not found`);\n }\n\n try {\n await scheduler.persistJobEnabled(id, enabled, actorOf(c));\n } catch (err) {\n logger.error(`[cron] Could not save the enabled state of \"${id}\"`, { error: err });\n throw ApiError.serviceUnavailable(\n `The change to cron job \"${id}\" could not be saved, so no process was changed. ` +\n \"The server log has the reason.\"\n );\n }\n\n return c.json({ job: await scheduler.fetchJob(id) });\n });\n\n return router;\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ 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-D5Y3N-S4.js";
|
|
7
7
|
import { a as redactSensitiveText, r as logger, s as stringifyForLog } from "./logger-D-S-hO5e.js";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
import { hostname } from "node:os";
|
|
@@ -149,6 +149,8 @@ function buildScaleToZeroWarning(jobs, env = process.env) {
|
|
|
149
149
|
//#region src/cron/cron-scheduler.ts
|
|
150
150
|
var cron_scheduler_exports = /* @__PURE__ */ __exportAll({
|
|
151
151
|
CronScheduler: () => CronScheduler,
|
|
152
|
+
MAX_CAPTURED_LOG_BYTES: () => MAX_CAPTURED_LOG_BYTES,
|
|
153
|
+
MAX_CAPTURED_LOG_LINES: () => 500,
|
|
152
154
|
findMostRecentSlot: () => findMostRecentSlot,
|
|
153
155
|
isAlreadyExecutingSkip: () => isAlreadyExecutingSkip,
|
|
154
156
|
isValidTimeZone: () => isValidTimeZone,
|
|
@@ -295,6 +297,7 @@ function wallClockIn(candidate, zone) {
|
|
|
295
297
|
timeZone: zone,
|
|
296
298
|
hourCycle: "h23",
|
|
297
299
|
weekday: "short",
|
|
300
|
+
year: "numeric",
|
|
298
301
|
month: "numeric",
|
|
299
302
|
day: "numeric",
|
|
300
303
|
hour: "numeric",
|
|
@@ -305,6 +308,7 @@ function wallClockIn(candidate, zone) {
|
|
|
305
308
|
const parts = {};
|
|
306
309
|
for (const part of formatter.formatToParts(candidate)) parts[part.type] = part.value;
|
|
307
310
|
return {
|
|
311
|
+
year: Number(parts.year),
|
|
308
312
|
minute: Number(parts.minute),
|
|
309
313
|
hour: Number(parts.hour),
|
|
310
314
|
dom: Number(parts.day),
|
|
@@ -313,21 +317,69 @@ function wallClockIn(candidate, zone) {
|
|
|
313
317
|
};
|
|
314
318
|
}
|
|
315
319
|
/**
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* names its zone means the same wall-clock hour on every host it runs on.
|
|
320
|
+
* The wall clock an instant reads in `zone` when one is named, else in the
|
|
321
|
+
* process's own zone — which is whatever the host is set to, UTC in nearly
|
|
322
|
+
* every container. A schedule that names its zone means the same wall-clock
|
|
323
|
+
* hour on every host it runs on.
|
|
321
324
|
*/
|
|
322
|
-
function
|
|
323
|
-
const
|
|
325
|
+
function wallClockOf(instantMs, zone) {
|
|
326
|
+
const candidate = new Date(instantMs);
|
|
327
|
+
if (zone) return wallClockIn(candidate, zone);
|
|
328
|
+
return {
|
|
329
|
+
year: candidate.getFullYear(),
|
|
324
330
|
minute: candidate.getMinutes(),
|
|
325
331
|
hour: candidate.getHours(),
|
|
326
332
|
dom: candidate.getDate(),
|
|
327
333
|
month: candidate.getMonth() + 1,
|
|
328
334
|
dow: candidate.getDay()
|
|
329
335
|
};
|
|
330
|
-
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* The zone's offset from UTC at an instant: its wall clock read back as UTC,
|
|
339
|
+
* minus the instant. Two instants with the same offset have wall clocks
|
|
340
|
+
* exactly as far apart as the instants themselves.
|
|
341
|
+
*/
|
|
342
|
+
function offsetAt(instantMs, wall) {
|
|
343
|
+
return Date.UTC(wall.year, wall.month - 1, wall.dom, wall.hour, wall.minute) - instantMs;
|
|
344
|
+
}
|
|
345
|
+
/** Whether a wall-clock day — month, day of month, weekday — can hold a slot. */
|
|
346
|
+
function dayMatches(wall, fields) {
|
|
347
|
+
return fields.months.includes(wall.month) && fields.doms.includes(wall.dom) && fields.dows.includes(wall.dow);
|
|
348
|
+
}
|
|
349
|
+
/** Whether a wall-clock minute matches every field of the expression. */
|
|
350
|
+
function wallMatches(wall, fields) {
|
|
351
|
+
return dayMatches(wall, fields) && fields.hours.includes(wall.hour) && fields.minutes.includes(wall.minute);
|
|
352
|
+
}
|
|
353
|
+
var MINUTES_PER_DAY = 1440;
|
|
354
|
+
/**
|
|
355
|
+
* How many wall-clock minutes from a non-matching `wall` to the first later
|
|
356
|
+
* wall minute that could match. Never past the next wall midnight, so the
|
|
357
|
+
* minutes in between share `wall`'s day and every one of them is a miss.
|
|
358
|
+
*/
|
|
359
|
+
function wallMinutesToNextCandidate(wall, fields) {
|
|
360
|
+
const minuteOfDay = wall.hour * 60 + wall.minute;
|
|
361
|
+
if (!dayMatches(wall, fields)) return MINUTES_PER_DAY - minuteOfDay;
|
|
362
|
+
if (!fields.hours.includes(wall.hour)) {
|
|
363
|
+
const nextHour = fields.hours.find((hour) => hour > wall.hour);
|
|
364
|
+
return nextHour === void 0 ? MINUTES_PER_DAY - minuteOfDay : nextHour * 60 - minuteOfDay;
|
|
365
|
+
}
|
|
366
|
+
const nextMinute = fields.minutes.find((minute) => minute > wall.minute);
|
|
367
|
+
return nextMinute === void 0 ? 60 - wall.minute : nextMinute - wall.minute;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* The mirror of {@link wallMinutesToNextCandidate}: how many wall-clock
|
|
371
|
+
* minutes back from a non-matching `wall` to the latest earlier wall minute
|
|
372
|
+
* that could match. Never before 23:59 of the previous wall day.
|
|
373
|
+
*/
|
|
374
|
+
function wallMinutesToPreviousCandidate(wall, fields) {
|
|
375
|
+
const minuteOfDay = wall.hour * 60 + wall.minute;
|
|
376
|
+
if (!dayMatches(wall, fields)) return minuteOfDay + 1;
|
|
377
|
+
if (!fields.hours.includes(wall.hour)) {
|
|
378
|
+
const previousHour = [...fields.hours].reverse().find((hour) => hour < wall.hour);
|
|
379
|
+
return previousHour === void 0 ? minuteOfDay + 1 : minuteOfDay - (previousHour * 60 + 59);
|
|
380
|
+
}
|
|
381
|
+
const previousMinute = [...fields.minutes].reverse().find((minute) => minute < wall.minute);
|
|
382
|
+
return previousMinute === void 0 ? wall.minute + 1 : wall.minute - previousMinute;
|
|
331
383
|
}
|
|
332
384
|
/**
|
|
333
385
|
* How far forward to look for the next matching slot.
|
|
@@ -339,27 +391,86 @@ function matchesCronFields(candidate, fields, zone) {
|
|
|
339
391
|
var MAX_SLOT_SEARCH_MINUTES = 2105280;
|
|
340
392
|
var MINUTE_MS = 6e4;
|
|
341
393
|
/**
|
|
394
|
+
* The first real minute in `[startMs, endMs)`, stepping in `direction`, whose
|
|
395
|
+
* wall clock matches `fields` — the same answer as testing every minute in
|
|
396
|
+
* turn, without testing every minute.
|
|
397
|
+
*
|
|
398
|
+
* The search moves through *real* minutes and only reads the wall clock, never
|
|
399
|
+
* steps it: across a daylight-saving change a wall-clock minute is not a real
|
|
400
|
+
* one. Falling back, a wall-clock step from 02:59 summer time lands on 03:00
|
|
401
|
+
* winter time and skips the repeated hour; inside it, the "next" minute can be
|
|
402
|
+
* an hour in the past. That is why this used to test one real minute at a
|
|
403
|
+
* time — and a yearly job named in a zone cost half a million `Intl` calls,
|
|
404
|
+
* blocking the event loop for a third of a second on every re-arm, and two
|
|
405
|
+
* seconds for 29 February.
|
|
406
|
+
*
|
|
407
|
+
* Now it hops. From a miss, {@link wallMinutesToNextCandidate} says how many
|
|
408
|
+
* wall minutes cannot match (the rest of a day that is the wrong date, the
|
|
409
|
+
* rest of an hour that is the wrong hour, the minutes before the next listed
|
|
410
|
+
* one). While the zone's offset holds, that many wall minutes are exactly
|
|
411
|
+
* that many real minutes, so the hop is taken when the offset at both ends is
|
|
412
|
+
* the same. When it differs, a transition lies inside the hop: the search
|
|
413
|
+
* bisects for the first minute on the new offset and resumes there — every
|
|
414
|
+
* minute before it is on the old offset, so its wall clock is inside the
|
|
415
|
+
* stretch already known to miss. A hop is at most a day, and the one thing
|
|
416
|
+
* this assumes is that no zone changes its offset and changes it back within
|
|
417
|
+
* a single day, which none in the tz database does. Each day costs one
|
|
418
|
+
* wall-clock read, plus about a dozen for each transition crossed.
|
|
419
|
+
*
|
|
420
|
+
* `direction: -1` walks back from `startMs` to `endMs` (inclusive) with the
|
|
421
|
+
* mirror of the same hop, for {@link findMostRecentSlot}.
|
|
422
|
+
*/
|
|
423
|
+
function searchSlot(fields, startMs, endMs, direction, zone) {
|
|
424
|
+
const inWindow = (instantMs) => direction === 1 ? instantMs < endMs : instantMs >= endMs;
|
|
425
|
+
let current = startMs;
|
|
426
|
+
let wall = wallClockOf(current, zone);
|
|
427
|
+
while (inWindow(current)) {
|
|
428
|
+
if (wallMatches(wall, fields)) return current;
|
|
429
|
+
const skip = direction === 1 ? wallMinutesToNextCandidate(wall, fields) : wallMinutesToPreviousCandidate(wall, fields);
|
|
430
|
+
const target = current + direction * skip * MINUTE_MS;
|
|
431
|
+
const targetWall = wallClockOf(target, zone);
|
|
432
|
+
const offset = offsetAt(current, wall);
|
|
433
|
+
if (skip === 1 || offsetAt(target, targetWall) === offset) {
|
|
434
|
+
current = target;
|
|
435
|
+
wall = targetWall;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
let held = current;
|
|
439
|
+
let changed = target;
|
|
440
|
+
let changedWall = targetWall;
|
|
441
|
+
while (Math.abs(changed - held) > MINUTE_MS) {
|
|
442
|
+
const middle = held + direction * Math.floor(Math.abs(changed - held) / MINUTE_MS / 2) * MINUTE_MS;
|
|
443
|
+
const middleWall = wallClockOf(middle, zone);
|
|
444
|
+
if (offsetAt(middle, middleWall) === offset) held = middle;
|
|
445
|
+
else {
|
|
446
|
+
changed = middle;
|
|
447
|
+
changedWall = middleWall;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
current = changed;
|
|
451
|
+
wall = changedWall;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
342
455
|
* Calculate the next Date after `after` that matches the cron expression.
|
|
343
456
|
* Throws on invalid expressions.
|
|
344
457
|
*/
|
|
345
458
|
function parseCronExpression(expression, after, timezone) {
|
|
346
459
|
const fields = parseCronFields(expression);
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
candidate.setTime(candidate.getTime() + MINUTE_MS);
|
|
353
|
-
}
|
|
460
|
+
const start = new Date(after);
|
|
461
|
+
start.setUTCSeconds(0, 0);
|
|
462
|
+
const startMs = start.getTime() + MINUTE_MS;
|
|
463
|
+
const slot = searchSlot(fields, startMs, startMs + MAX_SLOT_SEARCH_MINUTES * MINUTE_MS, 1, timezone);
|
|
464
|
+
if (slot !== void 0) return new Date(slot);
|
|
354
465
|
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.`);
|
|
355
466
|
}
|
|
356
467
|
/**
|
|
357
468
|
* The latest slot matching `expression` within the inclusive window
|
|
358
469
|
* `[from, to]`, or `undefined` when the expression has no slot in it.
|
|
359
470
|
*
|
|
360
|
-
* Walks backwards
|
|
361
|
-
*
|
|
362
|
-
*
|
|
471
|
+
* Walks backwards from `to`, so the first hit is already the answer — in the
|
|
472
|
+
* common case (a job that ran normally moments ago) that is a handful of
|
|
473
|
+
* steps, not a scan of the whole window.
|
|
363
474
|
*
|
|
364
475
|
* `to`'s own minute is included: an instance booting at 06:00:30 has *not* run
|
|
365
476
|
* the 06:00 slot — `parseCronExpression` already skipped past it to tomorrow —
|
|
@@ -371,14 +482,46 @@ function parseCronExpression(expression, after, timezone) {
|
|
|
371
482
|
*/
|
|
372
483
|
function findMostRecentSlot(expression, from, to, timezone) {
|
|
373
484
|
const fields = parseCronFields(expression);
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
485
|
+
const start = new Date(to);
|
|
486
|
+
start.setUTCSeconds(0, 0);
|
|
487
|
+
const startMs = start.getTime();
|
|
488
|
+
const slot = searchSlot(fields, startMs, Math.max(from.getTime(), startMs - (MAX_SLOT_SEARCH_MINUTES - 1) * MINUTE_MS), -1, timezone);
|
|
489
|
+
return slot === void 0 ? void 0 : new Date(slot);
|
|
380
490
|
}
|
|
381
491
|
var MAX_LOGS_PER_JOB = 50;
|
|
492
|
+
var MAX_CAPTURED_LOG_BYTES = 64 * 1024;
|
|
493
|
+
/**
|
|
494
|
+
* A run's `ctx.log` capture, capped at {@link MAX_CAPTURED_LOG_LINES} and
|
|
495
|
+
* {@link MAX_CAPTURED_LOG_BYTES}. The kept lines are the first ones: once one
|
|
496
|
+
* line is dropped, every later one is too, so the history reads as a prefix of
|
|
497
|
+
* the run's output and never as lines with gaps between them.
|
|
498
|
+
*/
|
|
499
|
+
function createLogCapture(firstLine) {
|
|
500
|
+
const lines = [];
|
|
501
|
+
let bytes = 0;
|
|
502
|
+
let dropped = 0;
|
|
503
|
+
let closed = false;
|
|
504
|
+
const capture = {
|
|
505
|
+
add(line) {
|
|
506
|
+
if (closed) return;
|
|
507
|
+
const size = Buffer.byteLength(line, "utf8");
|
|
508
|
+
if (dropped === 0 && lines.length < 500 && bytes + size <= 65536) {
|
|
509
|
+
lines.push(line);
|
|
510
|
+
bytes += size;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
dropped++;
|
|
514
|
+
},
|
|
515
|
+
/** The lines kept, and the line that says what was not. Later `add`s are ignored. */
|
|
516
|
+
close() {
|
|
517
|
+
if (!closed && dropped > 0) lines.push(`… ${dropped} more line${dropped === 1 ? "" : "s"} not kept — see the server log`);
|
|
518
|
+
closed = true;
|
|
519
|
+
return lines;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
if (firstLine !== void 0) capture.add(firstLine);
|
|
523
|
+
return capture;
|
|
524
|
+
}
|
|
382
525
|
/**
|
|
383
526
|
* Minimum milliseconds between scheduled executions of the same job.
|
|
384
527
|
* Prevents tight re-execution loops caused by jitter or clock drift.
|
|
@@ -410,6 +553,12 @@ function setLongTimeout(callback, delayMs) {
|
|
|
410
553
|
/** A run's timeout when its definition names none. */
|
|
411
554
|
var DEFAULT_TIMEOUT_SECONDS = 300;
|
|
412
555
|
/**
|
|
556
|
+
* How often a started scheduler deletes slot claims and run history past
|
|
557
|
+
* retention. Both accrue one row per job per fire, so a scheduler that lives
|
|
558
|
+
* for months needs to prune while it runs, not only when it next boots.
|
|
559
|
+
*/
|
|
560
|
+
var CLAIM_SWEEP_INTERVAL_MS = 60 * 6e4;
|
|
561
|
+
/**
|
|
413
562
|
* How long a run lease outlasts the run's own timeout.
|
|
414
563
|
*
|
|
415
564
|
* The lease is timed on the database's clock from the moment it is taken, the
|
|
@@ -469,6 +618,10 @@ var CronScheduler = class {
|
|
|
469
618
|
* ever clear the lease that run took.
|
|
470
619
|
*/
|
|
471
620
|
instanceLabel = `${hostname()}:${process.pid}`;
|
|
621
|
+
/** The hourly claim- and log-retention sweep, while the scheduler is started with a store. */
|
|
622
|
+
claimSweepTimer;
|
|
623
|
+
sweepingClaims = false;
|
|
624
|
+
sweepingLogs = false;
|
|
472
625
|
/**
|
|
473
626
|
* Set the server singleton to make it available to cron job handlers.
|
|
474
627
|
*
|
|
@@ -559,8 +712,48 @@ var CronScheduler = class {
|
|
|
559
712
|
if (!this.store) logger.warn("[cron] No cron store attached — runs are uncoordinated; with multiple app instances every instance will execute every job");
|
|
560
713
|
this.warnIfScaleToZero();
|
|
561
714
|
this.catchUpMissedSlots();
|
|
715
|
+
if (this.store?.sweepClaims || this.store?.sweepLogs) {
|
|
716
|
+
const sweep = () => {
|
|
717
|
+
this.sweepClaims();
|
|
718
|
+
this.sweepLogs();
|
|
719
|
+
};
|
|
720
|
+
sweep();
|
|
721
|
+
this.claimSweepTimer = setInterval(sweep, CLAIM_SWEEP_INTERVAL_MS);
|
|
722
|
+
this.claimSweepTimer.unref?.();
|
|
723
|
+
}
|
|
562
724
|
logger.info(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);
|
|
563
725
|
}
|
|
726
|
+
/** Delete slot claims past retention. Failures are logged; the next hour tries again. */
|
|
727
|
+
async sweepClaims() {
|
|
728
|
+
const store = this.store;
|
|
729
|
+
if (!store?.sweepClaims || this.sweepingClaims) return;
|
|
730
|
+
this.sweepingClaims = true;
|
|
731
|
+
try {
|
|
732
|
+
await store.sweepClaims();
|
|
733
|
+
} catch (err) {
|
|
734
|
+
logger.warn("[cron] The claim retention sweep failed; it runs again within the hour", { error: err });
|
|
735
|
+
} finally {
|
|
736
|
+
this.sweepingClaims = false;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Delete run history past retention, and count the runs not counted yet.
|
|
741
|
+
* Failures are logged; the next hour tries again, from where this one
|
|
742
|
+
* stopped — each batch it finished is committed.
|
|
743
|
+
*/
|
|
744
|
+
async sweepLogs() {
|
|
745
|
+
const store = this.store;
|
|
746
|
+
if (!store?.sweepLogs || this.sweepingLogs) return;
|
|
747
|
+
this.sweepingLogs = true;
|
|
748
|
+
try {
|
|
749
|
+
const { counted } = await store.sweepLogs();
|
|
750
|
+
if (counted > 0) logger.info(`[cron] Counted ${counted} earlier run(s) into the jobs' run totals`);
|
|
751
|
+
} catch (err) {
|
|
752
|
+
logger.warn("[cron] The run history sweep failed; it runs again within the hour", { error: err });
|
|
753
|
+
} finally {
|
|
754
|
+
this.sweepingLogs = false;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
564
757
|
/**
|
|
565
758
|
* Stop the scheduler: clear every timer, then wait for the runs executing
|
|
566
759
|
* right now — for at most `timeoutMs` when given.
|
|
@@ -576,6 +769,10 @@ var CronScheduler = class {
|
|
|
576
769
|
async stop(timeoutMs) {
|
|
577
770
|
this.started = false;
|
|
578
771
|
for (const [id] of this.jobs) this.stopJob(id);
|
|
772
|
+
if (this.claimSweepTimer) {
|
|
773
|
+
clearInterval(this.claimSweepTimer);
|
|
774
|
+
this.claimSweepTimer = void 0;
|
|
775
|
+
}
|
|
579
776
|
if (this.inFlight.size === 0) return;
|
|
580
777
|
const settled = Promise.allSettled([...this.inFlight]).then(() => true);
|
|
581
778
|
if (timeoutMs === void 0) {
|
|
@@ -747,6 +944,46 @@ var CronScheduler = class {
|
|
|
747
944
|
return this.executeJob(job, true);
|
|
748
945
|
}
|
|
749
946
|
/**
|
|
947
|
+
* Start a job now and answer as soon as the run has begun, not when it
|
|
948
|
+
* ends — the manual trigger's `?async=1`.
|
|
949
|
+
*
|
|
950
|
+
* The synchronous {@link triggerJob} holds its HTTP request open for the
|
|
951
|
+
* whole run, and a proxy in front of the server (many stop at 60 s) cuts
|
|
952
|
+
* a long job's answer while the run carries on. This resolves once the
|
|
953
|
+
* run lease is held and the handler is about to be called, so a run that
|
|
954
|
+
* cannot start — already executing here or on the lease holder — is still
|
|
955
|
+
* answered as the skip it is. The run itself is tracked like any other:
|
|
956
|
+
* `stop()` waits for it, and its outcome lands in the run history, where
|
|
957
|
+
* the entry carries `manual: true` and the `startedAt` returned here.
|
|
958
|
+
*
|
|
959
|
+
* Returns `undefined` if the job doesn't exist.
|
|
960
|
+
*/
|
|
961
|
+
async startJob(id) {
|
|
962
|
+
const job = this.jobs.get(id);
|
|
963
|
+
if (!job) return void 0;
|
|
964
|
+
if (job.executing) {
|
|
965
|
+
logger.warn(`[cron] Skipping manual trigger of "${id}" — already executing`);
|
|
966
|
+
return {
|
|
967
|
+
started: false,
|
|
968
|
+
log: this.recordSkip(job, "already_executing", true)
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
let onStart;
|
|
972
|
+
const begun = new Promise((resolve) => {
|
|
973
|
+
onStart = resolve;
|
|
974
|
+
});
|
|
975
|
+
const run = this.executeJob(job, true, void 0, onStart);
|
|
976
|
+
const first = await Promise.race([begun.then((startedAt) => ({ startedAt })), run.then((log) => ({ log }))]);
|
|
977
|
+
if ("startedAt" in first) return {
|
|
978
|
+
started: true,
|
|
979
|
+
startedAt: first.startedAt
|
|
980
|
+
};
|
|
981
|
+
return {
|
|
982
|
+
started: false,
|
|
983
|
+
log: first.log
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
750
987
|
* Record a run that did not happen because the previous one had not
|
|
751
988
|
* finished.
|
|
752
989
|
*
|
|
@@ -899,7 +1136,7 @@ var CronScheduler = class {
|
|
|
899
1136
|
if (!this.started) {
|
|
900
1137
|
const now = /* @__PURE__ */ new Date();
|
|
901
1138
|
for (const job of jobs) try {
|
|
902
|
-
viewOf(job.id).nextRunAt =
|
|
1139
|
+
viewOf(job.id).nextRunAt = this.listedNextRun(job, now);
|
|
903
1140
|
} catch {}
|
|
904
1141
|
}
|
|
905
1142
|
if (store?.fetchRunSummaries && ids.length > 0) try {
|
|
@@ -911,6 +1148,34 @@ var CronScheduler = class {
|
|
|
911
1148
|
return views;
|
|
912
1149
|
}
|
|
913
1150
|
/**
|
|
1151
|
+
* The next slot of `job` after `now`, for a listing on a process whose
|
|
1152
|
+
* scheduler is not started — the `api` role, which Studio's Cron panel
|
|
1153
|
+
* polls every 15 seconds.
|
|
1154
|
+
*
|
|
1155
|
+
* Cached on the job. The key is the job's schedule and zone (a re-register
|
|
1156
|
+
* replaces the job, and with it the cache); the entry holds while
|
|
1157
|
+
* `computedAt <= now < slot`. That is the same answer, not an
|
|
1158
|
+
* approximation: the search found no matching minute between
|
|
1159
|
+
* `computedAt`'s next minute and `slot`, so from any later instant before
|
|
1160
|
+
* `slot` it lands on `slot` again. A clock that moves back, or reaching the
|
|
1161
|
+
* slot, recomputes. The cache holds a pure function of the schedule, so
|
|
1162
|
+
* there is nothing user- or role-scoped in it.
|
|
1163
|
+
*/
|
|
1164
|
+
listedNextRun(job, now) {
|
|
1165
|
+
const { schedule, timezone } = job.definition;
|
|
1166
|
+
const nowMs = now.getTime();
|
|
1167
|
+
const cached = job.listedSlot;
|
|
1168
|
+
if (cached && cached.schedule === schedule && cached.timezone === timezone && cached.computedAt <= nowMs && nowMs < cached.slot.getTime()) return cached.slot;
|
|
1169
|
+
const slot = parseCronExpression(schedule, now, timezone);
|
|
1170
|
+
job.listedSlot = {
|
|
1171
|
+
schedule,
|
|
1172
|
+
timezone,
|
|
1173
|
+
computedAt: nowMs,
|
|
1174
|
+
slot
|
|
1175
|
+
};
|
|
1176
|
+
return slot;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
914
1179
|
* Warn once at start when the process looks like it is running on a
|
|
915
1180
|
* platform that freezes or evicts instances between requests, where the
|
|
916
1181
|
* in-process timers this scheduler relies on never fire.
|
|
@@ -1064,10 +1329,10 @@ var CronScheduler = class {
|
|
|
1064
1329
|
* - Persists to store (non-blocking) if available
|
|
1065
1330
|
* - Always restores state even on catastrophic errors
|
|
1066
1331
|
*/
|
|
1067
|
-
executeJob(job, manual, seedLog) {
|
|
1332
|
+
executeJob(job, manual, seedLog, onStart) {
|
|
1068
1333
|
const abort = new AbortController();
|
|
1069
1334
|
this.runControllers.add(abort);
|
|
1070
|
-
const run = this.runJob(job, manual, abort, seedLog);
|
|
1335
|
+
const run = this.runJob(job, manual, abort, seedLog, onStart);
|
|
1071
1336
|
this.track(run);
|
|
1072
1337
|
return run.finally(() => {
|
|
1073
1338
|
this.runControllers.delete(abort);
|
|
@@ -1081,7 +1346,7 @@ var CronScheduler = class {
|
|
|
1081
1346
|
};
|
|
1082
1347
|
work.then(release, release);
|
|
1083
1348
|
}
|
|
1084
|
-
async runJob(job, manual, abort, seedLog) {
|
|
1349
|
+
async runJob(job, manual, abort, seedLog, onStart) {
|
|
1085
1350
|
if (job.executing) return this.recordSkip(job, "already_executing", manual);
|
|
1086
1351
|
job.executing = true;
|
|
1087
1352
|
const lease = await this.acquireLease(job);
|
|
@@ -1091,14 +1356,14 @@ var CronScheduler = class {
|
|
|
1091
1356
|
return this.recordSkip(job, "already_executing", manual, lease.holder ?? "another process");
|
|
1092
1357
|
}
|
|
1093
1358
|
const startedAt = /* @__PURE__ */ new Date();
|
|
1094
|
-
const capturedLogs = seedLog
|
|
1359
|
+
const capturedLogs = createLogCapture(seedLog || void 0);
|
|
1095
1360
|
const ctx = {
|
|
1096
1361
|
jobId: job.id,
|
|
1097
1362
|
scheduledAt: startedAt,
|
|
1098
1363
|
signal: abort.signal,
|
|
1099
1364
|
log: (...args) => {
|
|
1100
1365
|
const line = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
1101
|
-
capturedLogs.
|
|
1366
|
+
capturedLogs.add(line);
|
|
1102
1367
|
logger.info(args.map((a) => typeof a === "string" ? a : stringifyForLog(a)).join(" "), { cron: job.id });
|
|
1103
1368
|
},
|
|
1104
1369
|
rebase: this.client
|
|
@@ -1106,6 +1371,7 @@ var CronScheduler = class {
|
|
|
1106
1371
|
job.state = "running";
|
|
1107
1372
|
job.lastRunAt = startedAt;
|
|
1108
1373
|
job.totalRuns++;
|
|
1374
|
+
onStart?.(startedAt.toISOString());
|
|
1109
1375
|
let success = true;
|
|
1110
1376
|
let error;
|
|
1111
1377
|
let result;
|
|
@@ -1144,7 +1410,7 @@ var CronScheduler = class {
|
|
|
1144
1410
|
success,
|
|
1145
1411
|
error,
|
|
1146
1412
|
result: result !== void 0 ? result : void 0,
|
|
1147
|
-
logs: capturedLogs,
|
|
1413
|
+
logs: capturedLogs.close(),
|
|
1148
1414
|
manual
|
|
1149
1415
|
};
|
|
1150
1416
|
job.logs.push(logEntry);
|
|
@@ -1188,4 +1454,4 @@ var CronScheduler = class {
|
|
|
1188
1454
|
//#endregion
|
|
1189
1455
|
export { parseEnvBoolean as a, validateCronExpression as i, cron_scheduler_exports as n, isAlreadyExecutingSkip as r, CronScheduler as t };
|
|
1190
1456
|
|
|
1191
|
-
//# sourceMappingURL=cron-scheduler-
|
|
1457
|
+
//# sourceMappingURL=cron-scheduler-idtLPoN-.js.map
|