@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
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
|
|
6
|
+
import "./src-D5Y3N-S4.js";
|
|
7
|
+
import { t as isSQLAdmin } from "./backend-DTAOsLQc.js";
|
|
8
|
+
import { r as logger } from "./logger-D-S-hO5e.js";
|
|
9
|
+
import { n as revokeInternalTableSql, t as REBASE_USER_ROLE } from "./internal-tables-L2t39RdT.js";
|
|
10
|
+
import { n as createDdlBootstrapper, r as hasInCauseChain } from "./ddl-bootstrap-BaqMSa4Y.js";
|
|
11
|
+
//#region src/cron/cron-store.ts
|
|
12
|
+
var cron_store_exports = /* @__PURE__ */ __exportAll({ createCronStore: () => createCronStore });
|
|
13
|
+
var TABLE = "rebase.cron_logs";
|
|
14
|
+
var CLAIMS_TABLE = "rebase.cron_claims";
|
|
15
|
+
var STATE_TABLE = "rebase.cron_job_state";
|
|
16
|
+
/** `$1, $2, …` for `count` values, starting at `$first`. */
|
|
17
|
+
function placeholders(count, first = 1) {
|
|
18
|
+
return Array.from({ length: count }, (_, i) => `$${first + i}`).join(", ");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Claims older than this are garbage-collected — all but each job's most
|
|
22
|
+
* recent one, which is kept however old it is; see `sweepClaims`.
|
|
23
|
+
*/
|
|
24
|
+
var CLAIM_RETENTION_DAYS = 7;
|
|
25
|
+
/** Claims one retention statement deletes; a sweep repeats it until a batch comes back short. */
|
|
26
|
+
var CLAIM_SWEEP_BATCH = 5e3;
|
|
27
|
+
/**
|
|
28
|
+
* How far ahead a claim may legitimately sit. Slots are claimed as they fire,
|
|
29
|
+
* so anything beyond this is a clock-skewed peer at best and a stranded claim
|
|
30
|
+
* at worst; see the sweep in `ensureTable`.
|
|
31
|
+
*/
|
|
32
|
+
var FUTURE_CLAIM_SKEW_MINUTES = 2;
|
|
33
|
+
/**
|
|
34
|
+
* How long a run stays in `cron_logs`, which is what the history panel and
|
|
35
|
+
* `GET /api/admin/cron/:id/logs` show. The run and failure counts do not
|
|
36
|
+
* shrink with it: they are kept on `cron_job_state` for all time.
|
|
37
|
+
*
|
|
38
|
+
* Nothing used to delete from `cron_logs`: twelve ordinary jobs wrote 1.5M rows
|
|
39
|
+
* and 617 MB a year, and the counts were `count(*)` over all of it on every
|
|
40
|
+
* poll of the Studio panel.
|
|
41
|
+
*/
|
|
42
|
+
var LOG_RETENTION_DAYS = 30;
|
|
43
|
+
/** History rows one retention or counting statement handles; a sweep repeats it until a batch comes back short. */
|
|
44
|
+
var LOG_SWEEP_BATCH = 5e3;
|
|
45
|
+
/**
|
|
46
|
+
* Whether the run being folded in is at least as recent as the job's last
|
|
47
|
+
* one. The last-run fields only move forward: a long run is logged when it
|
|
48
|
+
* ends, after the skip that started while it ran, and a counting batch can
|
|
49
|
+
* hold any run of the job's history.
|
|
50
|
+
*/
|
|
51
|
+
var IS_NEWER_RUN = "(s.last_run_at IS NULL OR EXCLUDED.last_run_at >= s.last_run_at)";
|
|
52
|
+
/**
|
|
53
|
+
* Add a set of runs to their jobs' counters on `cron_job_state`, as one
|
|
54
|
+
* `INSERT … ON CONFLICT`. `runs` names a relation of the runs —
|
|
55
|
+
* `job_id, started_at, duration_ms, success, error` — and becomes one row per
|
|
56
|
+
* job: how many, how many failed, and the most recent of them.
|
|
57
|
+
*
|
|
58
|
+
* Ordered by job, so two statements folding the same jobs lock their rows in
|
|
59
|
+
* the same order and cannot deadlock.
|
|
60
|
+
*/
|
|
61
|
+
function foldIntoCounters(runs) {
|
|
62
|
+
return `
|
|
63
|
+
INSERT INTO ${STATE_TABLE} AS s
|
|
64
|
+
(job_id, total_runs, total_failures, last_run_at, last_duration_ms, last_success, last_error)
|
|
65
|
+
SELECT tally.job_id, tally.runs, tally.failures,
|
|
66
|
+
latest.started_at, latest.duration_ms, latest.success, latest.error
|
|
67
|
+
FROM (
|
|
68
|
+
SELECT job_id, count(*) AS runs, count(*) FILTER (WHERE NOT success) AS failures
|
|
69
|
+
FROM ${runs} GROUP BY job_id
|
|
70
|
+
) AS tally
|
|
71
|
+
JOIN (
|
|
72
|
+
SELECT DISTINCT ON (job_id) job_id, started_at, duration_ms, success, error
|
|
73
|
+
FROM ${runs} ORDER BY job_id, started_at DESC
|
|
74
|
+
) AS latest ON latest.job_id = tally.job_id
|
|
75
|
+
ORDER BY tally.job_id
|
|
76
|
+
ON CONFLICT (job_id) DO UPDATE SET
|
|
77
|
+
total_runs = s.total_runs + EXCLUDED.total_runs,
|
|
78
|
+
total_failures = s.total_failures + EXCLUDED.total_failures,
|
|
79
|
+
last_run_at = CASE WHEN ${IS_NEWER_RUN} THEN EXCLUDED.last_run_at ELSE s.last_run_at END,
|
|
80
|
+
last_duration_ms = CASE WHEN ${IS_NEWER_RUN} THEN EXCLUDED.last_duration_ms ELSE s.last_duration_ms END,
|
|
81
|
+
last_success = CASE WHEN ${IS_NEWER_RUN} THEN EXCLUDED.last_success ELSE s.last_success END,
|
|
82
|
+
last_error = CASE WHEN ${IS_NEWER_RUN} THEN EXCLUDED.last_error ELSE s.last_error END`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* SQLSTATEs that mean the counters are not there to write to — a column or
|
|
86
|
+
* table a failed boot did not create, or a privilege it did not get. A log
|
|
87
|
+
* insert that meets one is written without its count; the next sweep counts
|
|
88
|
+
* it. Anything else is not retried, because a statement that failed after it
|
|
89
|
+
* committed would then be logged — and counted — twice.
|
|
90
|
+
*/
|
|
91
|
+
var COUNTERS_MISSING_SQLSTATES = /* @__PURE__ */ new Set([
|
|
92
|
+
"42703",
|
|
93
|
+
"42P01",
|
|
94
|
+
"42501"
|
|
95
|
+
]);
|
|
96
|
+
/**
|
|
97
|
+
* Detect a unique-constraint violation anywhere in an error's cause chain.
|
|
98
|
+
* Match the SQLSTATE code, never message text. Also covers SQLite
|
|
99
|
+
* ("UNIQUE constraint failed") and MySQL (ER_DUP_ENTRY 1062) for future SQL
|
|
100
|
+
* drivers.
|
|
101
|
+
*
|
|
102
|
+
* Distinct from `isConcurrentDdlRace` in `boot/ddl-bootstrap.ts`, which shares
|
|
103
|
+
* the 23505 code but asks a different question — that one is about a losing
|
|
104
|
+
* `CREATE`, this one is about a losing claim, and only the latter means
|
|
105
|
+
* "another instance already has this slot".
|
|
106
|
+
*/
|
|
107
|
+
function isUniqueViolation(err) {
|
|
108
|
+
return hasInCauseChain(err, (e) => e.code === "23505" || e.errno === 1062 || typeof e.message === "string" && e.message.includes("UNIQUE constraint failed"));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The setup each database is going through right now, in this process. The
|
|
112
|
+
* scheduler, the expired-token sweep and the history retention sweep each make
|
|
113
|
+
* a store on the same driver, and three setups at once ran the same REVOKEs
|
|
114
|
+
* concurrently — which Postgres can answer with "tuple concurrently updated",
|
|
115
|
+
* failing a security control at boot. Keyed on the driver, so a second
|
|
116
|
+
* database gets a setup of its own.
|
|
117
|
+
*
|
|
118
|
+
* Only the attempt in flight is shared. A later call runs the setup again —
|
|
119
|
+
* every statement in it is idempotent — because the tables can be gone by
|
|
120
|
+
* then: a database reset under a running server, or a suite that drops the
|
|
121
|
+
* schema between tests. A process that remembered "set up" would never bring
|
|
122
|
+
* them back.
|
|
123
|
+
*/
|
|
124
|
+
var setUpsInFlight = /* @__PURE__ */ new WeakMap();
|
|
125
|
+
/** Databases this process has already said "ready" for, so it says so once. */
|
|
126
|
+
var announcedReady = /* @__PURE__ */ new WeakSet();
|
|
127
|
+
function ensureSharedInFlight(driver, setUp) {
|
|
128
|
+
const current = setUpsInFlight.get(driver);
|
|
129
|
+
if (current) return current;
|
|
130
|
+
const attempt = setUp().then(() => void 0).finally(() => {
|
|
131
|
+
if (setUpsInFlight.get(driver) === attempt) setUpsInFlight.delete(driver);
|
|
132
|
+
});
|
|
133
|
+
setUpsInFlight.set(driver, attempt);
|
|
134
|
+
return attempt;
|
|
135
|
+
}
|
|
136
|
+
function createCronStore(driver) {
|
|
137
|
+
const admin = driver.admin;
|
|
138
|
+
if (!isSQLAdmin(admin)) {
|
|
139
|
+
logger.warn("⚠️ [cron-store] DataDriver does not support SQL admin — cron logs will not be persisted.");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const exec = (sqlText, options) => admin.executeSql(sqlText, options?.params ? { params: options.params } : void 0);
|
|
143
|
+
const ddl = createDdlBootstrapper(exec, "cron-store");
|
|
144
|
+
/** Whether `rebase.<table>` has `column`. False when it cannot tell, so the idempotent ALTER still runs. */
|
|
145
|
+
const hasColumn = async (table, column) => {
|
|
146
|
+
try {
|
|
147
|
+
return (await exec(`SELECT 1 FROM information_schema.columns
|
|
148
|
+
WHERE table_schema = 'rebase' AND table_name = $1 AND column_name = $2`, { params: [table, column] }) ?? []).length > 0;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Whether a boot has nothing to do: the three tables, the three indexes and
|
|
155
|
+
* the run-counter columns are there, the dropped index is gone, and the
|
|
156
|
+
* end-user role holds no privilege on any of the tables. One catalogue
|
|
157
|
+
* read, where every boot used to run eleven statements — `CREATE … IF NOT
|
|
158
|
+
* EXISTS` takes its table's lock before it finds the object there, and a
|
|
159
|
+
* `REVOKE` rewrites the table's catalogue row even when nothing is granted.
|
|
160
|
+
* Anything missing, or a read that fails, and the boot runs them all.
|
|
161
|
+
*/
|
|
162
|
+
const bootIsCurrent = async () => {
|
|
163
|
+
const objects = [
|
|
164
|
+
"cron_logs",
|
|
165
|
+
"cron_claims",
|
|
166
|
+
"cron_job_state",
|
|
167
|
+
"idx_cron_logs_job",
|
|
168
|
+
"idx_cron_logs_uncounted",
|
|
169
|
+
"idx_cron_logs_started"
|
|
170
|
+
];
|
|
171
|
+
const columns = ["cron_logs.counted", ...[
|
|
172
|
+
"total_runs",
|
|
173
|
+
"total_failures",
|
|
174
|
+
"last_run_at",
|
|
175
|
+
"last_duration_ms",
|
|
176
|
+
"last_success",
|
|
177
|
+
"last_error"
|
|
178
|
+
].map((c) => `cron_job_state.${c}`)];
|
|
179
|
+
const quoted = (names) => names.map((name) => `'${name}'`).join(", ");
|
|
180
|
+
try {
|
|
181
|
+
const row = (await exec(`
|
|
182
|
+
WITH objects AS (
|
|
183
|
+
SELECT c.oid, c.relname, c.relkind, c.relrowsecurity, c.relacl FROM pg_class c
|
|
184
|
+
WHERE c.relnamespace = to_regnamespace('rebase')
|
|
185
|
+
), target AS (
|
|
186
|
+
SELECT oid FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}'
|
|
187
|
+
)
|
|
188
|
+
SELECT
|
|
189
|
+
(SELECT count(*) FROM objects WHERE relname IN (${quoted(objects)}))::int AS present,
|
|
190
|
+
(SELECT count(*) FROM objects WHERE relname = 'idx_cron_logs_job_failures')::int AS stale,
|
|
191
|
+
(SELECT count(*) FROM information_schema.columns
|
|
192
|
+
WHERE table_schema = 'rebase' AND table_name || '.' || column_name IN (${quoted(columns)}))::int AS columns,
|
|
193
|
+
(SELECT count(*) FROM objects o
|
|
194
|
+
WHERE o.relkind = 'r' AND o.relname IN ('cron_logs', 'cron_claims', 'cron_job_state')
|
|
195
|
+
AND NOT o.relrowsecurity AND EXISTS (SELECT 1 FROM target)
|
|
196
|
+
AND (o.relacl IS NULL OR EXISTS (
|
|
197
|
+
SELECT 1 FROM aclexplode(o.relacl) a WHERE a.grantee = (SELECT oid FROM target)
|
|
198
|
+
)))::int AS granted
|
|
199
|
+
`))?.[0];
|
|
200
|
+
return Number(row?.present) === objects.length && Number(row?.stale) === 0 && Number(row?.columns) === columns.length && Number(row?.granted) === 0;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Drop claims for slots that have not happened yet. A slot is claimed at
|
|
207
|
+
* the moment it fires, so a future one can only come from a timer that
|
|
208
|
+
* woke early — and because claims are permanent, that claim would silently
|
|
209
|
+
* skip the real run when it finally came due. The margin keeps a legitimate
|
|
210
|
+
* claim made moments early by a clock-skewed peer.
|
|
211
|
+
*/
|
|
212
|
+
const releaseFutureClaims = () => ddl.step("Future-slot claim sweep", async () => {
|
|
213
|
+
const stranded = await exec(`DELETE FROM ${CLAIMS_TABLE}
|
|
214
|
+
WHERE slot > now() + make_interval(mins => $1)
|
|
215
|
+
RETURNING job_id, slot`, { params: [FUTURE_CLAIM_SKEW_MINUTES] });
|
|
216
|
+
for (const row of stranded ?? []) logger.warn(`[cron-store] Released a claim on the future slot ${new Date(row.slot).toISOString()} for "${row.job_id}" — it was claimed by a timer that fired early, and would otherwise have skipped that run`);
|
|
217
|
+
});
|
|
218
|
+
/** Run one sweep statement until a batch comes back short. Resolves with the total of `column`. */
|
|
219
|
+
const inBatches = async (sqlText, params, column) => {
|
|
220
|
+
let total = 0;
|
|
221
|
+
for (;;) {
|
|
222
|
+
const rows = await exec(sqlText, { params });
|
|
223
|
+
const done = Number((rows?.[0])?.[column] ?? 0);
|
|
224
|
+
total += done;
|
|
225
|
+
if (done < LOG_SWEEP_BATCH) return total;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* The boot setup. Resolves with whether every table came up and was locked
|
|
230
|
+
* away; anything short of that has been reported, and is worth another try.
|
|
231
|
+
*/
|
|
232
|
+
const announceReady = () => {
|
|
233
|
+
if (announcedReady.has(driver)) return;
|
|
234
|
+
announcedReady.add(driver);
|
|
235
|
+
logger.info("✅ Cron logs table ready");
|
|
236
|
+
};
|
|
237
|
+
const setUp = async () => {
|
|
238
|
+
if (await bootIsCurrent()) {
|
|
239
|
+
await releaseFutureClaims();
|
|
240
|
+
announceReady();
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
await ddl.ensureObject("Creating schema rebase", "CREATE SCHEMA IF NOT EXISTS rebase");
|
|
244
|
+
await ddl.ensureObject(`Creating ${TABLE}`, `
|
|
245
|
+
CREATE TABLE IF NOT EXISTS ${TABLE} (
|
|
246
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
247
|
+
job_id TEXT NOT NULL,
|
|
248
|
+
started_at TIMESTAMPTZ NOT NULL,
|
|
249
|
+
finished_at TIMESTAMPTZ NOT NULL,
|
|
250
|
+
duration_ms INTEGER NOT NULL,
|
|
251
|
+
success BOOLEAN NOT NULL DEFAULT true,
|
|
252
|
+
error TEXT,
|
|
253
|
+
result JSONB,
|
|
254
|
+
logs JSONB,
|
|
255
|
+
manual BOOLEAN NOT NULL DEFAULT false
|
|
256
|
+
)
|
|
257
|
+
`);
|
|
258
|
+
await ddl.ensureObject("Creating idx_cron_logs_job", `
|
|
259
|
+
CREATE INDEX IF NOT EXISTS idx_cron_logs_job
|
|
260
|
+
ON ${TABLE}(job_id, started_at DESC)
|
|
261
|
+
`);
|
|
262
|
+
if (!await hasColumn("cron_logs", "counted")) await ddl.ensureObject(`Adding ${TABLE}.counted`, `
|
|
263
|
+
ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS counted BOOLEAN
|
|
264
|
+
`);
|
|
265
|
+
await ddl.ensureObject("Creating idx_cron_logs_uncounted", `
|
|
266
|
+
CREATE INDEX IF NOT EXISTS idx_cron_logs_uncounted
|
|
267
|
+
ON ${TABLE}(job_id, started_at DESC) WHERE counted IS NULL
|
|
268
|
+
`);
|
|
269
|
+
await ddl.ensureObject("Creating idx_cron_logs_started", `
|
|
270
|
+
CREATE INDEX IF NOT EXISTS idx_cron_logs_started
|
|
271
|
+
ON ${TABLE}(started_at)
|
|
272
|
+
`);
|
|
273
|
+
await ddl.ensureObject("Dropping idx_cron_logs_job_failures", "DROP INDEX IF EXISTS rebase.idx_cron_logs_job_failures");
|
|
274
|
+
await ddl.ensureObject(`Creating ${CLAIMS_TABLE}`, `
|
|
275
|
+
CREATE TABLE IF NOT EXISTS ${CLAIMS_TABLE} (
|
|
276
|
+
job_id TEXT NOT NULL,
|
|
277
|
+
slot TIMESTAMPTZ NOT NULL,
|
|
278
|
+
claimed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
279
|
+
PRIMARY KEY (job_id, slot)
|
|
280
|
+
)
|
|
281
|
+
`);
|
|
282
|
+
await ddl.ensureObject(`Creating ${STATE_TABLE}`, `
|
|
283
|
+
CREATE TABLE IF NOT EXISTS ${STATE_TABLE} (
|
|
284
|
+
job_id TEXT PRIMARY KEY,
|
|
285
|
+
enabled BOOLEAN,
|
|
286
|
+
updated_at TIMESTAMPTZ,
|
|
287
|
+
updated_by TEXT,
|
|
288
|
+
running_until TIMESTAMPTZ,
|
|
289
|
+
running_by TEXT
|
|
290
|
+
)
|
|
291
|
+
`);
|
|
292
|
+
if (!await hasColumn("cron_job_state", "total_runs")) await ddl.ensureObject(`Adding the run counters to ${STATE_TABLE}`, `
|
|
293
|
+
ALTER TABLE ${STATE_TABLE}
|
|
294
|
+
ADD COLUMN IF NOT EXISTS total_runs BIGINT NOT NULL DEFAULT 0,
|
|
295
|
+
ADD COLUMN IF NOT EXISTS total_failures BIGINT NOT NULL DEFAULT 0,
|
|
296
|
+
ADD COLUMN IF NOT EXISTS last_run_at TIMESTAMPTZ,
|
|
297
|
+
ADD COLUMN IF NOT EXISTS last_duration_ms INTEGER,
|
|
298
|
+
ADD COLUMN IF NOT EXISTS last_success BOOLEAN,
|
|
299
|
+
ADD COLUMN IF NOT EXISTS last_error TEXT
|
|
300
|
+
`);
|
|
301
|
+
const [logsReady, claimsReady, stateReady] = await Promise.all([
|
|
302
|
+
ddl.isReadable(TABLE),
|
|
303
|
+
ddl.isReadable(CLAIMS_TABLE),
|
|
304
|
+
ddl.isReadable(STATE_TABLE)
|
|
305
|
+
]);
|
|
306
|
+
if (claimsReady) await releaseFutureClaims();
|
|
307
|
+
if (logsReady) await ddl.step("Revoking end-user access to cron_logs", () => exec(revokeInternalTableSql("rebase", "cron_logs")));
|
|
308
|
+
if (claimsReady) await ddl.step("Revoking end-user access to cron_claims", () => exec(revokeInternalTableSql("rebase", "cron_claims")));
|
|
309
|
+
if (stateReady) await ddl.step("Revoking end-user access to cron_job_state", () => exec(revokeInternalTableSql("rebase", "cron_job_state")));
|
|
310
|
+
if (logsReady && claimsReady && stateReady) {
|
|
311
|
+
announceReady();
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
if (!claimsReady) logger.error(`❌ [cron-store] ${CLAIMS_TABLE} is unavailable — scheduled runs cannot be coordinated. With more than one app instance, every instance will now run every job on every tick.`);
|
|
315
|
+
if (!logsReady) logger.warn(`⚠️ [cron-store] ${TABLE} is unavailable — cron run history will not be persisted.`);
|
|
316
|
+
if (!stateReady) logger.warn(`⚠️ [cron-store] ${STATE_TABLE} is unavailable — pausing a job reaches only the process that served the request, and jobs run as their code declares on every other one. A manual trigger is not kept off a run another process is making.`);
|
|
317
|
+
return false;
|
|
318
|
+
};
|
|
319
|
+
return {
|
|
320
|
+
ensureTable: () => ensureSharedInFlight(driver, setUp),
|
|
321
|
+
async sweepClaims() {
|
|
322
|
+
let total = 0;
|
|
323
|
+
for (;;) {
|
|
324
|
+
const rows = await exec(`WITH swept AS (
|
|
325
|
+
DELETE FROM ${CLAIMS_TABLE}
|
|
326
|
+
WHERE (job_id, slot) IN (
|
|
327
|
+
SELECT c.job_id, c.slot FROM ${CLAIMS_TABLE} AS c
|
|
328
|
+
WHERE c.claimed_at < now() - make_interval(days => $1)
|
|
329
|
+
AND c.slot < (
|
|
330
|
+
SELECT max(latest.slot) FROM ${CLAIMS_TABLE} AS latest
|
|
331
|
+
WHERE latest.job_id = c.job_id
|
|
332
|
+
AND latest.slot <= now() + make_interval(mins => $2)
|
|
333
|
+
)
|
|
334
|
+
LIMIT $3
|
|
335
|
+
FOR UPDATE SKIP LOCKED
|
|
336
|
+
)
|
|
337
|
+
RETURNING 1
|
|
338
|
+
)
|
|
339
|
+
SELECT count(*)::int AS deleted FROM swept`, { params: [
|
|
340
|
+
CLAIM_RETENTION_DAYS,
|
|
341
|
+
FUTURE_CLAIM_SKEW_MINUTES,
|
|
342
|
+
CLAIM_SWEEP_BATCH
|
|
343
|
+
] });
|
|
344
|
+
const deleted = Number((rows?.[0])?.deleted ?? 0);
|
|
345
|
+
total += deleted;
|
|
346
|
+
if (deleted < CLAIM_SWEEP_BATCH) return total;
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
async sweepLogs() {
|
|
350
|
+
return {
|
|
351
|
+
deleted: await inBatches(`WITH gone AS (
|
|
352
|
+
DELETE FROM ${TABLE}
|
|
353
|
+
WHERE id IN (
|
|
354
|
+
SELECT id FROM ${TABLE}
|
|
355
|
+
WHERE started_at < now() - make_interval(days => $1)
|
|
356
|
+
ORDER BY started_at
|
|
357
|
+
LIMIT $2
|
|
358
|
+
FOR UPDATE SKIP LOCKED
|
|
359
|
+
)
|
|
360
|
+
RETURNING job_id, counted, started_at, duration_ms, success, error
|
|
361
|
+
), uncounted AS (
|
|
362
|
+
SELECT job_id, started_at, duration_ms, success, error FROM gone WHERE counted IS NULL
|
|
363
|
+
), folded AS (
|
|
364
|
+
${foldIntoCounters("uncounted")}
|
|
365
|
+
RETURNING 1
|
|
366
|
+
)
|
|
367
|
+
SELECT count(*)::int AS deleted FROM gone`, [LOG_RETENTION_DAYS, LOG_SWEEP_BATCH], "deleted"),
|
|
368
|
+
counted: await inBatches(`WITH batch AS (
|
|
369
|
+
SELECT id FROM ${TABLE}
|
|
370
|
+
WHERE counted IS NULL
|
|
371
|
+
ORDER BY job_id, started_at DESC
|
|
372
|
+
LIMIT $1
|
|
373
|
+
FOR UPDATE SKIP LOCKED
|
|
374
|
+
), marked AS (
|
|
375
|
+
UPDATE ${TABLE} AS l SET counted = true
|
|
376
|
+
FROM batch WHERE l.id = batch.id
|
|
377
|
+
RETURNING l.job_id, l.started_at, l.duration_ms, l.success, l.error
|
|
378
|
+
), folded AS (
|
|
379
|
+
${foldIntoCounters("marked")}
|
|
380
|
+
RETURNING 1
|
|
381
|
+
)
|
|
382
|
+
SELECT count(*)::int AS counted FROM marked`, [LOG_SWEEP_BATCH], "counted")
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
async insertLog(entry) {
|
|
386
|
+
const resultJson = entry.result !== void 0 ? JSON.stringify(entry.result) : null;
|
|
387
|
+
const logsJson = entry.logs.length > 0 ? JSON.stringify(entry.logs) : null;
|
|
388
|
+
const params = [
|
|
389
|
+
entry.jobId,
|
|
390
|
+
entry.startedAt,
|
|
391
|
+
entry.finishedAt,
|
|
392
|
+
entry.durationMs,
|
|
393
|
+
entry.success,
|
|
394
|
+
entry.error || null,
|
|
395
|
+
resultJson,
|
|
396
|
+
logsJson,
|
|
397
|
+
entry.manual
|
|
398
|
+
];
|
|
399
|
+
const columns = "job_id, started_at, finished_at, duration_ms, success, error, result, logs, manual";
|
|
400
|
+
const values = "$1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, $9";
|
|
401
|
+
try {
|
|
402
|
+
try {
|
|
403
|
+
await exec(`WITH logged AS (
|
|
404
|
+
INSERT INTO ${TABLE} (${columns}, counted)
|
|
405
|
+
VALUES (${values}, true)
|
|
406
|
+
RETURNING job_id, started_at, duration_ms, success, error
|
|
407
|
+
)
|
|
408
|
+
${foldIntoCounters("logged")}`, { params });
|
|
409
|
+
} catch (err) {
|
|
410
|
+
if (!hasInCauseChain(err, (e) => typeof e.code === "string" && COUNTERS_MISSING_SQLSTATES.has(e.code))) throw err;
|
|
411
|
+
await exec(`INSERT INTO ${TABLE} (${columns}) VALUES (${values})`, { params });
|
|
412
|
+
}
|
|
413
|
+
} catch (err) {
|
|
414
|
+
logger.error(`[cron-store] Failed to persist log for "${entry.jobId}"`, { error: err });
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
async fetchLogs(jobId, limit = 50) {
|
|
418
|
+
return (await exec(`SELECT job_id, started_at, finished_at, duration_ms, success, error, result, logs, manual
|
|
419
|
+
FROM ${TABLE}
|
|
420
|
+
WHERE job_id = $1
|
|
421
|
+
ORDER BY started_at DESC
|
|
422
|
+
LIMIT $2`, { params: [jobId, limit] })).map(rowToLogEntry);
|
|
423
|
+
},
|
|
424
|
+
async fetchJobStats() {
|
|
425
|
+
const stats = /* @__PURE__ */ new Map();
|
|
426
|
+
try {
|
|
427
|
+
const rows = await exec(`
|
|
428
|
+
SELECT job_id, total_runs, total_failures, last_run_at
|
|
429
|
+
FROM ${STATE_TABLE}
|
|
430
|
+
WHERE total_runs > 0
|
|
431
|
+
`);
|
|
432
|
+
for (const row of rows ?? []) stats.set(String(row.job_id), {
|
|
433
|
+
totalRuns: Number(row.total_runs),
|
|
434
|
+
totalFailures: Number(row.total_failures),
|
|
435
|
+
lastRunAt: toIsoString(row.last_run_at)
|
|
436
|
+
});
|
|
437
|
+
} catch (err) {
|
|
438
|
+
logger.error("[cron-store] Failed to fetch job stats", { error: err });
|
|
439
|
+
}
|
|
440
|
+
return stats;
|
|
441
|
+
},
|
|
442
|
+
async tryClaimRun(jobId, slot) {
|
|
443
|
+
try {
|
|
444
|
+
return (await exec(`INSERT INTO ${CLAIMS_TABLE} (job_id, slot)
|
|
445
|
+
VALUES ($1, $2)
|
|
446
|
+
ON CONFLICT (job_id, slot) DO NOTHING
|
|
447
|
+
RETURNING job_id`, { params: [jobId, slot] })).length > 0;
|
|
448
|
+
} catch (err) {
|
|
449
|
+
if (isUniqueViolation(err)) return false;
|
|
450
|
+
throw err;
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
async saveEnabledOverride(jobId, enabled, updatedBy) {
|
|
454
|
+
await exec(`INSERT INTO ${STATE_TABLE} (job_id, enabled, updated_at, updated_by)
|
|
455
|
+
VALUES ($1, $2::boolean, now(), $3)
|
|
456
|
+
ON CONFLICT (job_id) DO UPDATE
|
|
457
|
+
SET enabled = EXCLUDED.enabled, updated_at = now(), updated_by = EXCLUDED.updated_by`, { params: [
|
|
458
|
+
jobId,
|
|
459
|
+
enabled,
|
|
460
|
+
updatedBy ?? null
|
|
461
|
+
] });
|
|
462
|
+
},
|
|
463
|
+
async fetchJobStates(jobIds) {
|
|
464
|
+
const states = /* @__PURE__ */ new Map();
|
|
465
|
+
if (jobIds.length === 0) return states;
|
|
466
|
+
const rows = await exec(`SELECT job_id, enabled,
|
|
467
|
+
CASE WHEN running_until > now() THEN running_by END AS running_by
|
|
468
|
+
FROM ${STATE_TABLE}
|
|
469
|
+
WHERE job_id IN (${placeholders(jobIds.length)})`, { params: [...jobIds] });
|
|
470
|
+
for (const row of rows ?? []) states.set(String(row.job_id), {
|
|
471
|
+
enabled: typeof row.enabled === "boolean" ? row.enabled : null,
|
|
472
|
+
runningBy: typeof row.running_by === "string" ? row.running_by : void 0
|
|
473
|
+
});
|
|
474
|
+
return states;
|
|
475
|
+
},
|
|
476
|
+
async tryAcquireRunLease(jobId, holder, ttlSeconds) {
|
|
477
|
+
const row = (await exec(`WITH attempt AS (
|
|
478
|
+
INSERT INTO ${STATE_TABLE} AS s (job_id, running_until, running_by)
|
|
479
|
+
VALUES ($1, now() + make_interval(secs => $2::double precision), $3)
|
|
480
|
+
ON CONFLICT (job_id) DO UPDATE
|
|
481
|
+
SET running_until = EXCLUDED.running_until, running_by = EXCLUDED.running_by
|
|
482
|
+
WHERE s.running_until IS NULL OR s.running_until <= now()
|
|
483
|
+
RETURNING s.job_id
|
|
484
|
+
)
|
|
485
|
+
SELECT EXISTS (SELECT 1 FROM attempt) AS acquired,
|
|
486
|
+
(SELECT running_by FROM ${STATE_TABLE}
|
|
487
|
+
WHERE job_id = $1 AND running_until > now()) AS holder`, { params: [
|
|
488
|
+
jobId,
|
|
489
|
+
ttlSeconds,
|
|
490
|
+
holder
|
|
491
|
+
] }))?.[0];
|
|
492
|
+
if (!row) throw new Error(`The run lease for "${jobId}" got no answer from the database`);
|
|
493
|
+
if (row.acquired === true) return { acquired: true };
|
|
494
|
+
return {
|
|
495
|
+
acquired: false,
|
|
496
|
+
holder: typeof row.holder === "string" ? row.holder : void 0
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
async releaseRunLease(jobId, holder) {
|
|
500
|
+
await exec(`UPDATE ${STATE_TABLE}
|
|
501
|
+
SET running_until = NULL, running_by = NULL
|
|
502
|
+
WHERE job_id = $1 AND running_by = $2`, { params: [jobId, holder] });
|
|
503
|
+
},
|
|
504
|
+
async fetchRunSummaries(jobIds) {
|
|
505
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
506
|
+
if (jobIds.length === 0) return summaries;
|
|
507
|
+
const rows = await exec(`SELECT j.job_id,
|
|
508
|
+
COALESCE(s.total_runs, 0) + COALESCE(pending.runs, 0) AS total_runs,
|
|
509
|
+
COALESCE(s.total_failures, 0) + COALESCE(pending.failures, 0) AS total_failures,
|
|
510
|
+
CASE WHEN newer.later THEN pending_last.started_at ELSE s.last_run_at END AS started_at,
|
|
511
|
+
CASE WHEN newer.later THEN pending_last.duration_ms ELSE s.last_duration_ms END AS duration_ms,
|
|
512
|
+
CASE WHEN newer.later THEN pending_last.success ELSE s.last_success END AS success,
|
|
513
|
+
CASE WHEN newer.later THEN pending_last.error ELSE s.last_error END AS error
|
|
514
|
+
FROM (VALUES ${jobIds.map((_, i) => `($${i + 1}::text)`).join(", ")}) AS j(job_id)
|
|
515
|
+
LEFT JOIN ${STATE_TABLE} AS s ON s.job_id = j.job_id
|
|
516
|
+
LEFT JOIN LATERAL (
|
|
517
|
+
SELECT count(*) AS runs, count(*) FILTER (WHERE NOT l.success) AS failures
|
|
518
|
+
FROM ${TABLE} l
|
|
519
|
+
WHERE l.job_id = j.job_id AND l.counted IS NULL
|
|
520
|
+
) AS pending ON true
|
|
521
|
+
LEFT JOIN LATERAL (
|
|
522
|
+
SELECT l.started_at, l.duration_ms, l.success, l.error
|
|
523
|
+
FROM ${TABLE} l
|
|
524
|
+
WHERE l.job_id = j.job_id AND l.counted IS NULL
|
|
525
|
+
ORDER BY l.started_at DESC
|
|
526
|
+
LIMIT 1
|
|
527
|
+
) AS pending_last ON true
|
|
528
|
+
CROSS JOIN LATERAL (
|
|
529
|
+
SELECT pending_last.started_at IS NOT NULL
|
|
530
|
+
AND (s.last_run_at IS NULL OR pending_last.started_at >= s.last_run_at) AS later
|
|
531
|
+
) AS newer`, { params: [...jobIds] });
|
|
532
|
+
for (const row of rows ?? []) {
|
|
533
|
+
const summary = {
|
|
534
|
+
totalRuns: Number(row.total_runs ?? 0),
|
|
535
|
+
totalFailures: Number(row.total_failures ?? 0)
|
|
536
|
+
};
|
|
537
|
+
const lastRunAt = toIsoString(row.started_at);
|
|
538
|
+
if (lastRunAt) {
|
|
539
|
+
summary.lastRunAt = lastRunAt;
|
|
540
|
+
summary.lastDurationMs = Number(row.duration_ms);
|
|
541
|
+
summary.lastSuccess = row.success === true;
|
|
542
|
+
if (typeof row.error === "string") summary.lastError = row.error;
|
|
543
|
+
}
|
|
544
|
+
summaries.set(String(row.job_id), summary);
|
|
545
|
+
}
|
|
546
|
+
return summaries;
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
/** A TIMESTAMPTZ as the driver returned it — a `Date` or a string — in ISO form. */
|
|
551
|
+
function toIsoString(value) {
|
|
552
|
+
if (value instanceof Date) return value.toISOString();
|
|
553
|
+
if (typeof value === "string" || typeof value === "number") return new Date(value).toISOString();
|
|
554
|
+
}
|
|
555
|
+
function rowToLogEntry(row) {
|
|
556
|
+
return {
|
|
557
|
+
jobId: row.job_id,
|
|
558
|
+
startedAt: new Date(row.started_at).toISOString(),
|
|
559
|
+
finishedAt: new Date(row.finished_at).toISOString(),
|
|
560
|
+
durationMs: row.duration_ms,
|
|
561
|
+
success: row.success,
|
|
562
|
+
error: row.error ?? void 0,
|
|
563
|
+
result: row.result ?? void 0,
|
|
564
|
+
logs: Array.isArray(row.logs) ? row.logs : row.logs ? (() => {
|
|
565
|
+
try {
|
|
566
|
+
return JSON.parse(row.logs);
|
|
567
|
+
} catch {
|
|
568
|
+
return [];
|
|
569
|
+
}
|
|
570
|
+
})() : [],
|
|
571
|
+
manual: row.manual
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
//#endregion
|
|
575
|
+
export { cron_store_exports as n, createCronStore as t };
|
|
576
|
+
|
|
577
|
+
//# sourceMappingURL=cron-store-CZ30ZBi-.js.map
|