@cronwatch/sdk 0.3.1 → 0.4.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 +8 -2
- package/dist/anthropic.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/bugsnag.cjs +108 -0
- package/dist/bugsnag.cjs.map +1 -0
- package/dist/bugsnag.d.cts +26 -0
- package/dist/bugsnag.d.ts +26 -0
- package/dist/bugsnag.js +106 -0
- package/dist/bugsnag.js.map +1 -0
- package/dist/client-B7bqnkI5.d.cts +450 -0
- package/dist/client-pmqH3apS.d.ts +450 -0
- package/dist/d1.cjs +214 -0
- package/dist/d1.cjs.map +1 -0
- package/dist/d1.d.cts +42 -0
- package/dist/d1.d.ts +42 -0
- package/dist/d1.js +212 -0
- package/dist/d1.js.map +1 -0
- package/dist/datadog.cjs +95 -0
- package/dist/datadog.cjs.map +1 -0
- package/dist/datadog.d.cts +24 -0
- package/dist/datadog.d.ts +24 -0
- package/dist/datadog.js +93 -0
- package/dist/datadog.js.map +1 -0
- package/dist/discord.cjs +2 -0
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +1 -1
- package/dist/discord.d.ts +1 -1
- package/dist/discord.js +2 -0
- package/dist/discord.js.map +1 -1
- package/dist/email-BJ-z_epD.d.cts +21 -0
- package/dist/email-_H0faCh3.d.ts +21 -0
- package/dist/honeybadger.cjs +101 -0
- package/dist/honeybadger.cjs.map +1 -0
- package/dist/honeybadger.d.cts +23 -0
- package/dist/honeybadger.d.ts +23 -0
- package/dist/honeybadger.js +99 -0
- package/dist/honeybadger.js.map +1 -0
- package/dist/index.cjs +462 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -260
- package/dist/index.d.ts +5 -260
- package/dist/index.js +462 -45
- package/dist/index.js.map +1 -1
- package/dist/mailgun.cjs +115 -0
- package/dist/mailgun.cjs.map +1 -0
- package/dist/mailgun.d.cts +21 -0
- package/dist/mailgun.d.ts +21 -0
- package/dist/mailgun.js +113 -0
- package/dist/mailgun.js.map +1 -0
- package/dist/newrelic.cjs +88 -0
- package/dist/newrelic.cjs.map +1 -0
- package/dist/newrelic.d.cts +25 -0
- package/dist/newrelic.d.ts +25 -0
- package/dist/newrelic.js +86 -0
- package/dist/newrelic.js.map +1 -0
- package/dist/node.cjs +191 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +97 -0
- package/dist/node.d.ts +97 -0
- package/dist/node.js +186 -0
- package/dist/node.js.map +1 -0
- package/dist/pg-cron.cjs +286 -0
- package/dist/pg-cron.cjs.map +1 -0
- package/dist/pg-cron.d.cts +93 -0
- package/dist/pg-cron.d.ts +93 -0
- package/dist/pg-cron.js +280 -0
- package/dist/pg-cron.js.map +1 -0
- package/dist/postgres.cjs +20 -0
- package/dist/postgres.cjs.map +1 -1
- package/dist/postgres.d.cts +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.js +20 -0
- package/dist/postgres.js.map +1 -1
- package/dist/postmark.cjs +106 -0
- package/dist/postmark.cjs.map +1 -0
- package/dist/postmark.d.cts +18 -0
- package/dist/postmark.d.ts +18 -0
- package/dist/postmark.js +104 -0
- package/dist/postmark.js.map +1 -0
- package/dist/resend.cjs +114 -0
- package/dist/resend.cjs.map +1 -0
- package/dist/resend.d.cts +16 -0
- package/dist/resend.d.ts +16 -0
- package/dist/resend.js +112 -0
- package/dist/resend.js.map +1 -0
- package/dist/rollbar.cjs +108 -0
- package/dist/rollbar.cjs.map +1 -0
- package/dist/rollbar.d.cts +20 -0
- package/dist/rollbar.d.ts +20 -0
- package/dist/rollbar.js +106 -0
- package/dist/rollbar.js.map +1 -0
- package/dist/sendgrid.cjs +114 -0
- package/dist/sendgrid.cjs.map +1 -0
- package/dist/sendgrid.d.cts +19 -0
- package/dist/sendgrid.d.ts +19 -0
- package/dist/sendgrid.js +112 -0
- package/dist/sendgrid.js.map +1 -0
- package/dist/sentry.cjs +130 -0
- package/dist/sentry.cjs.map +1 -0
- package/dist/sentry.d.cts +28 -0
- package/dist/sentry.d.ts +28 -0
- package/dist/sentry.js +127 -0
- package/dist/sentry.js.map +1 -0
- package/dist/ses.cjs +170 -0
- package/dist/ses.cjs.map +1 -0
- package/dist/ses.d.cts +25 -0
- package/dist/ses.d.ts +25 -0
- package/dist/ses.js +168 -0
- package/dist/ses.js.map +1 -0
- package/dist/slack.cjs +2 -0
- package/dist/slack.cjs.map +1 -1
- package/dist/slack.d.cts +1 -1
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +2 -0
- package/dist/slack.js.map +1 -1
- package/dist/sqlite.cjs +18 -0
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +1 -1
- package/dist/sqlite.d.ts +1 -1
- package/dist/sqlite.js +18 -0
- package/dist/sqlite.js.map +1 -1
- package/dist/twilio.cjs +168 -0
- package/dist/twilio.cjs.map +1 -0
- package/dist/twilio.d.cts +52 -0
- package/dist/twilio.d.ts +52 -0
- package/dist/twilio.js +162 -0
- package/dist/twilio.js.map +1 -0
- package/dist/{types-DUt2apl0.d.cts → types-Ddq1MUPL.d.cts} +29 -2
- package/dist/{types-DUt2apl0.d.ts → types-Ddq1MUPL.d.ts} +29 -2
- package/dist/webhook.cjs +11 -5
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +6 -3
- package/dist/webhook.d.ts +6 -3
- package/dist/webhook.js +11 -6
- package/dist/webhook.js.map +1 -1
- package/package.json +208 -2
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var croner = require('croner');
|
|
4
|
-
var crypto$1 = require('crypto');
|
|
5
4
|
|
|
6
5
|
// src/duration.ts
|
|
7
6
|
var UNIT_MS = {
|
|
@@ -55,6 +54,16 @@ function formatRelative(at, now) {
|
|
|
55
54
|
if (abs < 5e3) return text;
|
|
56
55
|
return diff < 0 ? `${text} ago` : `in ${text}`;
|
|
57
56
|
}
|
|
57
|
+
|
|
58
|
+
// src/env.ts
|
|
59
|
+
function readEnv(name) {
|
|
60
|
+
return typeof process === "undefined" || !process.env ? void 0 : process.env[name];
|
|
61
|
+
}
|
|
62
|
+
function isDevelopment() {
|
|
63
|
+
if (typeof process === "undefined" || !process.env) return false;
|
|
64
|
+
const mode = process.env.NODE_ENV;
|
|
65
|
+
return mode === "development" || mode === "test";
|
|
66
|
+
}
|
|
58
67
|
var cache = /* @__PURE__ */ new Map();
|
|
59
68
|
var crons = /* @__PURE__ */ new WeakMap();
|
|
60
69
|
var EARLY_SLACK_MS = 6e4;
|
|
@@ -307,7 +316,15 @@ function onRunFinish(def, run, state, history, now) {
|
|
|
307
316
|
function onCheck(def, stored, lastRun, state, now) {
|
|
308
317
|
const next = cloneState(state);
|
|
309
318
|
const alerts = [];
|
|
310
|
-
if (!def.schedule)
|
|
319
|
+
if (!def.schedule) {
|
|
320
|
+
const since = next.open.missed;
|
|
321
|
+
if (since !== void 0) {
|
|
322
|
+
delete next.open.missed;
|
|
323
|
+
next.pendingRecovery = (next.pendingRecovery ?? []).filter((c) => c !== "missed");
|
|
324
|
+
alerts.push({ type: "recovered", run: lastRun, details: { after: ["missed"], reason: "unscheduled", since } });
|
|
325
|
+
}
|
|
326
|
+
return { state: next, alerts, nextExpectedAt: null, dueAt: null };
|
|
327
|
+
}
|
|
311
328
|
const parsed = parseSchedule(def.schedule, def.timezone);
|
|
312
329
|
const grace = graceMs(def);
|
|
313
330
|
const lastRunAt = lastRun?.startedAt ?? null;
|
|
@@ -457,6 +474,12 @@ ${out}`);
|
|
|
457
474
|
break;
|
|
458
475
|
}
|
|
459
476
|
case "recovered": {
|
|
477
|
+
if (draft.details.reason === "unscheduled") {
|
|
478
|
+
title = `${name} is no longer scheduled`;
|
|
479
|
+
const since = draft.details.since;
|
|
480
|
+
lines.push(`${since === void 0 ? "" : `Missed since ${when(since, now)}. `}It has no schedule now, so nothing is due; the missed alert is closed.`);
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
460
483
|
title = `${name} recovered`;
|
|
461
484
|
const after = draft.details.after.map((c) => c.replace("_", " ")).join(", ");
|
|
462
485
|
lines.push(`A run ${run ? when(run.startedAt, now) : "just now"} succeeded${after ? ` after: ${after}` : ""}.`);
|
|
@@ -818,15 +841,17 @@ function messagePage(title, message, base) {
|
|
|
818
841
|
|
|
819
842
|
// src/routes/index.ts
|
|
820
843
|
var COOKIE = "cronwatch_token";
|
|
821
|
-
function cookieValue(token) {
|
|
822
|
-
|
|
844
|
+
async function cookieValue(token) {
|
|
845
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`cronwatch-cookie:${token}`));
|
|
846
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
823
847
|
}
|
|
824
848
|
var DEFAULT_RUNS = 20;
|
|
825
849
|
var MAX_RUNS = 500;
|
|
826
850
|
var CSP = "default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; base-uri 'none'";
|
|
827
851
|
var SECURITY_HEADERS = { "x-content-type-options": "nosniff", "referrer-policy": "same-origin", "x-robots-tag": "noindex" };
|
|
828
852
|
function developmentToken() {
|
|
829
|
-
|
|
853
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
854
|
+
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
830
855
|
}
|
|
831
856
|
function developmentSignInLine(origin, base, token) {
|
|
832
857
|
return `[cronwatch] CRONWATCH_TOKEN is not set, so this development server made a token for the dashboard. Sign in: ${origin}${base}/?token=${token}`;
|
|
@@ -847,6 +872,34 @@ function readCookie(request, name) {
|
|
|
847
872
|
}
|
|
848
873
|
return null;
|
|
849
874
|
}
|
|
875
|
+
function firstValue(value) {
|
|
876
|
+
const first = value?.split(",")[0]?.trim();
|
|
877
|
+
return first ? first : null;
|
|
878
|
+
}
|
|
879
|
+
function configuredOrigin(value) {
|
|
880
|
+
if (value === void 0 || value === "") return null;
|
|
881
|
+
let url;
|
|
882
|
+
try {
|
|
883
|
+
url = new URL(value);
|
|
884
|
+
} catch {
|
|
885
|
+
throw new Error(`routes: origin must be an absolute URL such as "https://app.example.com", got ${JSON.stringify(value)}`);
|
|
886
|
+
}
|
|
887
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`routes: origin must be http or https, got ${JSON.stringify(value)}`);
|
|
888
|
+
return url.origin;
|
|
889
|
+
}
|
|
890
|
+
function forwardedOrigin(request, url) {
|
|
891
|
+
const proto = firstValue(request.headers.get("x-forwarded-proto"))?.toLowerCase() ?? null;
|
|
892
|
+
const host = firstValue(request.headers.get("x-forwarded-host"));
|
|
893
|
+
if (proto === null && host === null) return url.origin;
|
|
894
|
+
if (proto !== null && proto !== "http" && proto !== "https") return url.origin;
|
|
895
|
+
try {
|
|
896
|
+
const built = new URL(`${proto ?? url.protocol.slice(0, -1)}://${host ?? url.host}`);
|
|
897
|
+
if (built.pathname !== "/" || built.username || built.password || built.search || built.hash) return url.origin;
|
|
898
|
+
return built.origin;
|
|
899
|
+
} catch {
|
|
900
|
+
return url.origin;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
850
903
|
function stripBase(pathname, base) {
|
|
851
904
|
let path = pathname.startsWith(base) ? pathname.slice(base.length) : pathname;
|
|
852
905
|
if (path === "") path = "/";
|
|
@@ -871,9 +924,9 @@ async function readBody(request) {
|
|
|
871
924
|
}
|
|
872
925
|
return {};
|
|
873
926
|
}
|
|
874
|
-
function crossSite(request,
|
|
927
|
+
function crossSite(request, publicOrigin) {
|
|
875
928
|
const origin = request.headers.get("origin");
|
|
876
|
-
if (origin !== null && origin !==
|
|
929
|
+
if (origin !== null && origin !== publicOrigin) return true;
|
|
877
930
|
const site = request.headers.get("sec-fetch-site");
|
|
878
931
|
return site !== null && site !== "same-origin" && site !== "none";
|
|
879
932
|
}
|
|
@@ -890,31 +943,37 @@ function runsLimit(value) {
|
|
|
890
943
|
}
|
|
891
944
|
function createRoutes(cw, options = {}) {
|
|
892
945
|
const optedOut = options.token === null;
|
|
893
|
-
const configured = optedOut ? null : options.token ||
|
|
946
|
+
const configured = optedOut ? null : options.token || readEnv("CRONWATCH_TOKEN") || null;
|
|
894
947
|
const base = (options.basePath ?? "/cronwatch").replace(/\/+$/, "");
|
|
895
|
-
const developing =
|
|
948
|
+
const developing = isDevelopment();
|
|
949
|
+
const fixedOrigin = configuredOrigin(options.origin);
|
|
950
|
+
const trustProxy = options.trustProxy === true;
|
|
951
|
+
const originOf = (request, url) => fixedOrigin ?? (trustProxy ? forwardedOrigin(request, url) : url.origin);
|
|
896
952
|
const generated = !configured && !optedOut && developing;
|
|
897
953
|
const token = generated ? developmentToken() : configured;
|
|
898
954
|
let announced = false;
|
|
955
|
+
let cookie = null;
|
|
956
|
+
const expectedCookie = (value) => cookie ??= cookieValue(value);
|
|
899
957
|
const serve = async (request, url, path, wantsHtml) => {
|
|
900
958
|
const method = request.method.toUpperCase();
|
|
959
|
+
const publicOrigin = originOf(request, url);
|
|
901
960
|
if (generated && !announced) {
|
|
902
961
|
announced = true;
|
|
903
|
-
console.info(developmentSignInLine(
|
|
962
|
+
console.info(developmentSignInLine(publicOrigin, base, token));
|
|
904
963
|
}
|
|
905
964
|
if (!token && !optedOut) {
|
|
906
965
|
return wantsHtml ? html(messagePage("CronWatch routes are locked", "Set CRONWATCH_TOKEN (or pass token to cw.routes()), or pass token: null to serve them open behind your own auth.", base), 503) : api({ ok: false, error: "CRONWATCH_TOKEN is not set" }, 503);
|
|
907
966
|
}
|
|
908
|
-
if (method !== "GET" && method !== "HEAD" && crossSite(request,
|
|
967
|
+
if (method !== "GET" && method !== "HEAD" && crossSite(request, publicOrigin)) {
|
|
909
968
|
return wantsHtml ? html(messagePage("Cross-site request refused", "Changes can only be made from the dashboard itself.", base), 403) : api({ ok: false, error: "Cross-site request refused" }, 403);
|
|
910
969
|
}
|
|
911
970
|
const bearer = request.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ?? null;
|
|
912
971
|
if (token) {
|
|
913
972
|
const query = wantsHtml && method === "GET" ? url.searchParams.get("token") : null;
|
|
914
|
-
const
|
|
973
|
+
const sent = readCookie(request, COOKIE);
|
|
915
974
|
const isCheck = path === "/api/check";
|
|
916
975
|
const cronSecretOk = isCheck && bearer !== null && cw.cronSecret !== null && constantTimeEqual(bearer, cw.cronSecret);
|
|
917
|
-
const tokenOk = bearer !== null ? constantTimeEqual(bearer, token) : query !== null ? constantTimeEqual(query, token) :
|
|
976
|
+
const tokenOk = bearer !== null ? constantTimeEqual(bearer, token) : query !== null ? constantTimeEqual(query, token) : sent !== null && constantTimeEqual(sent, await expectedCookie(token));
|
|
918
977
|
if (!cronSecretOk && !tokenOk) {
|
|
919
978
|
if (generated) {
|
|
920
979
|
return wantsHtml ? html(messagePage("Sign in", "CRONWATCH_TOKEN is not set, so this development server made a token. The sign-in link is in the server log: open it once and this browser stays signed in.", base), 401) : api({ ok: false, error: "Unauthorized: CRONWATCH_TOKEN is not set, so this development server made a token; it is in the server log" }, 401);
|
|
@@ -923,15 +982,15 @@ function createRoutes(cw, options = {}) {
|
|
|
923
982
|
}
|
|
924
983
|
if (query !== null) {
|
|
925
984
|
url.searchParams.delete("token");
|
|
926
|
-
const secure =
|
|
985
|
+
const secure = publicOrigin.startsWith("https:") ? "; Secure" : "";
|
|
927
986
|
return redirect(url.pathname + (url.search || ""), {
|
|
928
|
-
"set-cookie": `${COOKIE}=${
|
|
987
|
+
"set-cookie": `${COOKIE}=${await expectedCookie(token)}; Path=${base || "/"}; HttpOnly; SameSite=Lax; Max-Age=${60 * 60 * 24 * 30}${secure}`
|
|
929
988
|
});
|
|
930
989
|
}
|
|
931
990
|
}
|
|
932
991
|
const redirectBack = () => {
|
|
933
992
|
const referer = request.headers.get("referer") ?? "";
|
|
934
|
-
return redirect(referer.startsWith(
|
|
993
|
+
return redirect(referer.startsWith(publicOrigin + "/") ? referer : `${base}/`);
|
|
935
994
|
};
|
|
936
995
|
const decoded = path.split("/").filter(Boolean).map(safeDecode);
|
|
937
996
|
if (decoded.some((part) => part === null)) {
|
|
@@ -1092,6 +1151,7 @@ function memory() {
|
|
|
1092
1151
|
}
|
|
1093
1152
|
},
|
|
1094
1153
|
async insertRun(run) {
|
|
1154
|
+
if (runs.has(run.id)) throw new Error(`run ${run.id} already exists`);
|
|
1095
1155
|
runs.set(run.id, clone(run));
|
|
1096
1156
|
order.set(run.id, ++seq);
|
|
1097
1157
|
},
|
|
@@ -1101,6 +1161,13 @@ function memory() {
|
|
|
1101
1161
|
const { status, finishedAt, durationMs, error, output, metrics } = clone(run);
|
|
1102
1162
|
runs.set(run.id, { ...existing, status, finishedAt, durationMs, error, output, metrics });
|
|
1103
1163
|
},
|
|
1164
|
+
async updateRunIf(run, fromStatuses) {
|
|
1165
|
+
const existing = runs.get(run.id);
|
|
1166
|
+
if (!existing || !fromStatuses.includes(existing.status)) return false;
|
|
1167
|
+
const { status, finishedAt, durationMs, error, output, metrics } = clone(run);
|
|
1168
|
+
runs.set(run.id, { ...existing, status, finishedAt, durationMs, error, output, metrics });
|
|
1169
|
+
return true;
|
|
1170
|
+
},
|
|
1104
1171
|
async getRun(id) {
|
|
1105
1172
|
const r = runs.get(id);
|
|
1106
1173
|
return r ? clone(r) : null;
|
|
@@ -1196,6 +1263,7 @@ var CronWatch = class {
|
|
|
1196
1263
|
store;
|
|
1197
1264
|
alerts;
|
|
1198
1265
|
triage;
|
|
1266
|
+
sources;
|
|
1199
1267
|
/** The secret handler() requests must carry, or null when none is set. */
|
|
1200
1268
|
cronSecret;
|
|
1201
1269
|
retentionMs;
|
|
@@ -1211,6 +1279,8 @@ var CronWatch = class {
|
|
|
1211
1279
|
synced = /* @__PURE__ */ new Set();
|
|
1212
1280
|
/** The tail of each job's queue of state updates. See serial(). */
|
|
1213
1281
|
queues = /* @__PURE__ */ new Map();
|
|
1282
|
+
/** start() calls with an id still in flight, so two at once in this process record one run. */
|
|
1283
|
+
starting = /* @__PURE__ */ new Map();
|
|
1214
1284
|
ready = null;
|
|
1215
1285
|
checking = null;
|
|
1216
1286
|
lastPruneAt = 0;
|
|
@@ -1223,7 +1293,8 @@ var CronWatch = class {
|
|
|
1223
1293
|
this.store = options.store ?? (this.usingDefaultStore = true, memory());
|
|
1224
1294
|
this.alerts = options.alerts ?? [consoleChannel()];
|
|
1225
1295
|
this.triage = options.triage;
|
|
1226
|
-
|
|
1296
|
+
this.sources = options.sources ?? [];
|
|
1297
|
+
const secret = options.cronSecret === void 0 ? readEnv("CRON_SECRET") : options.cronSecret;
|
|
1227
1298
|
this.cronSecret = secret ? secret : null;
|
|
1228
1299
|
this.secretOptOut = options.cronSecret === null;
|
|
1229
1300
|
this.retentionMs = parseDuration(options.retention ?? "30d", "retention");
|
|
@@ -1267,6 +1338,12 @@ var CronWatch = class {
|
|
|
1267
1338
|
definedJobs() {
|
|
1268
1339
|
return [...this.definitions.values()];
|
|
1269
1340
|
}
|
|
1341
|
+
/** A handle on a run started elsewhere, as job(name).resume(runId). The job must be declared in this process. */
|
|
1342
|
+
resumeRun(name, runId) {
|
|
1343
|
+
const definition = this.definitions.get(name);
|
|
1344
|
+
if (!definition) return Promise.reject(new Error(`resumeRun: job "${name}" is not declared; call job() first`));
|
|
1345
|
+
return this.resumeHandle(definition, runId);
|
|
1346
|
+
}
|
|
1270
1347
|
handle(definition) {
|
|
1271
1348
|
const self = this;
|
|
1272
1349
|
return {
|
|
@@ -1305,6 +1382,12 @@ var CronWatch = class {
|
|
|
1305
1382
|
};
|
|
1306
1383
|
return json(body, outcome.run.status === "ok" ? 200 : 500);
|
|
1307
1384
|
};
|
|
1385
|
+
},
|
|
1386
|
+
start(options) {
|
|
1387
|
+
return self.startRun(definition, options ?? {});
|
|
1388
|
+
},
|
|
1389
|
+
resume(runId) {
|
|
1390
|
+
return self.resumeHandle(definition, runId);
|
|
1308
1391
|
}
|
|
1309
1392
|
};
|
|
1310
1393
|
}
|
|
@@ -1324,7 +1407,7 @@ var CronWatch = class {
|
|
|
1324
1407
|
if (!this.ready) {
|
|
1325
1408
|
this.ready = (async () => {
|
|
1326
1409
|
if (this.store.init) await this.store.init();
|
|
1327
|
-
if (this.usingDefaultStore &&
|
|
1410
|
+
if (this.usingDefaultStore && readEnv("NODE_ENV") === "production") {
|
|
1328
1411
|
console.warn("[cronwatch] using the in-memory store: runs and state are lost on restart. Pass a store from @cronwatch/sdk/sqlite or @cronwatch/sdk/postgres.");
|
|
1329
1412
|
}
|
|
1330
1413
|
})().catch((error) => {
|
|
@@ -1441,6 +1524,21 @@ var CronWatch = class {
|
|
|
1441
1524
|
run.durationMs = Math.max(0, finishedAt - startedAt);
|
|
1442
1525
|
run.metrics = recorder.metrics();
|
|
1443
1526
|
run.output = recorder.output() ?? (typeof result === "string" ? capOutput(result) : null);
|
|
1527
|
+
this.conclude(definition, run, result, error, threw, recorder.expectText() ?? (typeof result === "string" ? result : null));
|
|
1528
|
+
await started;
|
|
1529
|
+
try {
|
|
1530
|
+
const ignored = await this.recordFinish(definition, run, recorded, finishedAt);
|
|
1531
|
+
if (ignored) this.report(new Error(`run ${run.id} of ${name} ${ignored}; ignored`), `finishing ${name}`);
|
|
1532
|
+
} catch (e) {
|
|
1533
|
+
this.onError(e, `recording ${name}`);
|
|
1534
|
+
}
|
|
1535
|
+
return { run, result, error, threw };
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Sets a finished run's status and error from how it ended, then redacts
|
|
1539
|
+
* its output and error. Shared by execute() and RunHandle.finish().
|
|
1540
|
+
*/
|
|
1541
|
+
conclude(definition, run, result, error, threw, expectText) {
|
|
1444
1542
|
if (threw) {
|
|
1445
1543
|
run.status = "failed";
|
|
1446
1544
|
run.error = errorMessage(error);
|
|
@@ -1448,7 +1546,6 @@ var CronWatch = class {
|
|
|
1448
1546
|
run.status = "failed";
|
|
1449
1547
|
run.error = `HTTP ${result.status}${result.statusText ? ` ${result.statusText}` : ""}`;
|
|
1450
1548
|
} else {
|
|
1451
|
-
const expectText = recorder.expectText() ?? (typeof result === "string" ? result : null);
|
|
1452
1549
|
const unmet = checkExpectation(definition.expect, expectText);
|
|
1453
1550
|
if (unmet) {
|
|
1454
1551
|
run.status = "failed";
|
|
@@ -1459,44 +1556,336 @@ var CronWatch = class {
|
|
|
1459
1556
|
}
|
|
1460
1557
|
if (run.output !== null) run.output = stripNul(this.redact(run.output));
|
|
1461
1558
|
if (run.error !== null) run.error = stripNul(this.redact(run.error));
|
|
1462
|
-
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Writes a finished run and evaluates it. `recorded` says whether its
|
|
1562
|
+
* start was written; if not, it is inserted now. Returns why nothing was
|
|
1563
|
+
* recorded (another process finished the run first, say), or null. Throws
|
|
1564
|
+
* when the store does, so a handle can be finished again. Shared by
|
|
1565
|
+
* execute() and RunHandle.finish().
|
|
1566
|
+
*/
|
|
1567
|
+
async recordFinish(definition, run, recorded, finishedAt) {
|
|
1463
1568
|
if (!recorded) {
|
|
1569
|
+
await this.sync(definition);
|
|
1464
1570
|
try {
|
|
1465
|
-
await this.sync(definition);
|
|
1466
1571
|
await this.store.insertRun(run);
|
|
1467
|
-
|
|
1572
|
+
await this.finishRun(toStored(definition), run, finishedAt);
|
|
1573
|
+
return null;
|
|
1468
1574
|
} catch (e) {
|
|
1469
|
-
this.
|
|
1575
|
+
const stored = await this.store.getRun(run.id).catch(() => null);
|
|
1576
|
+
if (!stored) throw e;
|
|
1577
|
+
if (stored.job !== run.job) return `belongs to job "${stored.job}"`;
|
|
1470
1578
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1579
|
+
}
|
|
1580
|
+
const claim = await this.claimFinish(run);
|
|
1581
|
+
if ("ignored" in claim) return claim.ignored;
|
|
1582
|
+
if (!claim.lateAfterTimeout || run.status === "ok") await this.finishRun(toStored(definition), run, finishedAt);
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
/** A conditional write (Store.updateRunIf), or for a store without one, a read then a plain write. */
|
|
1586
|
+
async writeRunIf(run, fromStatuses) {
|
|
1587
|
+
if (typeof this.store.updateRunIf === "function") return this.store.updateRunIf(run, fromStatuses);
|
|
1588
|
+
const stored = await this.store.getRun(run.id);
|
|
1589
|
+
if (!stored || !fromStatuses.includes(stored.status)) return false;
|
|
1590
|
+
await this.store.updateRun(run);
|
|
1591
|
+
return true;
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Writes a finished run over its stored row, only while that row is still
|
|
1595
|
+
* running, or else still marked timeout by a check. Only the process whose
|
|
1596
|
+
* write lands goes on to evaluate the run; for the others it returns why
|
|
1597
|
+
* nothing was written. `lateAfterTimeout` means a check already counted
|
|
1598
|
+
* the run as a stuck failure: a late failure must not count twice, while
|
|
1599
|
+
* a late success still closes stuck and recovers. Throws when the store does.
|
|
1600
|
+
*/
|
|
1601
|
+
async claimFinish(run) {
|
|
1602
|
+
if (await this.writeRunIf(run, ["running"])) return { lateAfterTimeout: false };
|
|
1603
|
+
if (await this.writeRunIf(run, ["timeout"])) return { lateAfterTimeout: true };
|
|
1604
|
+
const stored = await this.store.getRun(run.id);
|
|
1605
|
+
return { ignored: stored ? `was already finished as ${stored.status}` : "was not found" };
|
|
1606
|
+
}
|
|
1607
|
+
/** job.start(): records a running run and returns a handle to finish it. See JobHandle.start. */
|
|
1608
|
+
async startRun(definition, options) {
|
|
1609
|
+
const id = options.id;
|
|
1610
|
+
if (id === void 0) return this.recordStart(definition, options.trigger);
|
|
1611
|
+
checkRunId(definition.name, id, "start");
|
|
1612
|
+
const key = `${definition.name}
|
|
1613
|
+
${id}`;
|
|
1614
|
+
const inFlight = this.starting.get(key);
|
|
1615
|
+
if (inFlight) return inFlight;
|
|
1616
|
+
const started = this.recordStart(definition, options.trigger, id).finally(() => {
|
|
1617
|
+
if (this.starting.get(key) === started) this.starting.delete(key);
|
|
1618
|
+
});
|
|
1619
|
+
this.starting.set(key, started);
|
|
1620
|
+
return started;
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* The start of execute() without the function: the run is inserted and
|
|
1624
|
+
* missed and stuck close (onRunStart). A store that fails is reported and
|
|
1625
|
+
* the handle inserts the finished run instead, as execute() does.
|
|
1626
|
+
*/
|
|
1627
|
+
async recordStart(definition, trigger = "start", id) {
|
|
1628
|
+
const name = definition.name;
|
|
1629
|
+
if (id !== void 0) {
|
|
1630
|
+
let stored = null;
|
|
1473
1631
|
try {
|
|
1474
|
-
await this.
|
|
1632
|
+
await this.ensureReady();
|
|
1633
|
+
stored = await this.store.getRun(id);
|
|
1475
1634
|
} catch (e) {
|
|
1476
1635
|
this.onError(e, `recording ${name}`);
|
|
1477
1636
|
}
|
|
1478
|
-
|
|
1479
|
-
await this.finishRun(toStored(definition), run, finishedAt, true);
|
|
1637
|
+
if (stored) return this.existingHandle(definition, stored);
|
|
1480
1638
|
}
|
|
1481
|
-
|
|
1639
|
+
const run = {
|
|
1640
|
+
id: id ?? crypto.randomUUID(),
|
|
1641
|
+
job: name,
|
|
1642
|
+
status: "running",
|
|
1643
|
+
startedAt: this.now(),
|
|
1644
|
+
finishedAt: null,
|
|
1645
|
+
durationMs: null,
|
|
1646
|
+
error: null,
|
|
1647
|
+
output: null,
|
|
1648
|
+
metrics: {},
|
|
1649
|
+
trigger
|
|
1650
|
+
};
|
|
1651
|
+
let recorded = false;
|
|
1652
|
+
try {
|
|
1653
|
+
await this.sync(definition);
|
|
1654
|
+
await this.store.insertRun({ ...run });
|
|
1655
|
+
recorded = true;
|
|
1656
|
+
} catch (e) {
|
|
1657
|
+
const stored = id === void 0 ? null : await this.store.getRun(id).catch(() => null);
|
|
1658
|
+
if (stored) return this.existingHandle(definition, stored);
|
|
1659
|
+
this.onError(e, `recording ${name}`);
|
|
1660
|
+
}
|
|
1661
|
+
if (recorded) {
|
|
1662
|
+
await this.updateState(name, (before) => ({ state: onRunStart(before), result: void 0 })).catch((e) => this.onError(e, `starting ${name}`));
|
|
1663
|
+
}
|
|
1664
|
+
return this.runHandle(definition, run.id, run, recorded, null);
|
|
1482
1665
|
}
|
|
1483
|
-
/**
|
|
1484
|
-
async
|
|
1485
|
-
|
|
1666
|
+
/** job.resume() and cw.resumeRun(). A store that cannot be read is reported, and finish() reads it again. */
|
|
1667
|
+
async resumeHandle(definition, runId) {
|
|
1668
|
+
checkRunId(definition.name, runId, "resume");
|
|
1669
|
+
let stored;
|
|
1486
1670
|
try {
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1671
|
+
await this.ensureReady();
|
|
1672
|
+
stored = await this.store.getRun(runId);
|
|
1673
|
+
} catch (e) {
|
|
1674
|
+
this.onError(e, `resuming ${definition.name}`);
|
|
1675
|
+
return this.runHandle(definition, runId, null, true, null);
|
|
1676
|
+
}
|
|
1677
|
+
if (!stored) return this.runHandle(definition, runId, null, true, "was not found");
|
|
1678
|
+
return this.existingHandle(definition, stored);
|
|
1679
|
+
}
|
|
1680
|
+
/** A handle on a stored run. One still running, or marked timeout by a check, can be finished. */
|
|
1681
|
+
existingHandle(definition, stored) {
|
|
1682
|
+
if (stored.job !== definition.name) {
|
|
1683
|
+
throw new Error(`run "${stored.id}" belongs to job "${stored.job}", not "${definition.name}"`);
|
|
1684
|
+
}
|
|
1685
|
+
const finished = stored.status === "ok" || stored.status === "failed";
|
|
1686
|
+
return this.runHandle(definition, stored.id, stored, true, finished ? `already finished as ${stored.status}` : null);
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* The handle itself. `base` is the run as last known here, `recorded`
|
|
1690
|
+
* whether its start is in the store, and `inactive` why finish() has
|
|
1691
|
+
* nothing to do, or null. Lines and metrics wait in the handle until
|
|
1692
|
+
* flush() or finish() merges them onto a fresh read of the stored run.
|
|
1693
|
+
*/
|
|
1694
|
+
runHandle(definition, id, base, recorded, inactive) {
|
|
1695
|
+
const self = this;
|
|
1696
|
+
const name = definition.name;
|
|
1697
|
+
const fresh = () => createRecorder({ id, job: name, startedAt: base?.startedAt ?? 0 });
|
|
1698
|
+
let recorder = fresh();
|
|
1699
|
+
let finished = inactive !== null;
|
|
1700
|
+
let finishCalled = false;
|
|
1701
|
+
let head = null;
|
|
1702
|
+
let queue = Promise.resolve();
|
|
1703
|
+
const inTurn = (fn) => {
|
|
1704
|
+
const result = queue.then(fn);
|
|
1705
|
+
queue = result.catch(() => {
|
|
1706
|
+
});
|
|
1707
|
+
return result;
|
|
1708
|
+
};
|
|
1709
|
+
const ignored = (why) => self.report(new Error(`run ${id} of ${name} ${why}; ignored`), `finishing ${name}`);
|
|
1710
|
+
const finish = (outcome) => {
|
|
1711
|
+
if (finishCalled) {
|
|
1712
|
+
ignored("was already finished by this handle");
|
|
1713
|
+
return Promise.resolve(null);
|
|
1714
|
+
}
|
|
1715
|
+
finishCalled = true;
|
|
1716
|
+
const wasInactive = finished;
|
|
1717
|
+
finished = true;
|
|
1718
|
+
const retryable = (error) => {
|
|
1719
|
+
finishCalled = false;
|
|
1720
|
+
finished = false;
|
|
1721
|
+
self.report(error, `finishing ${name}`);
|
|
1722
|
+
return null;
|
|
1723
|
+
};
|
|
1724
|
+
return inTurn(async () => {
|
|
1725
|
+
if (wasInactive) {
|
|
1726
|
+
ignored(inactive);
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
let from = base;
|
|
1730
|
+
if (recorded) {
|
|
1731
|
+
try {
|
|
1732
|
+
from = await self.store.getRun(id) ?? base;
|
|
1733
|
+
} catch (e) {
|
|
1734
|
+
return retryable(e);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (!from) {
|
|
1738
|
+
ignored("was not found");
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
if (from.job !== name) {
|
|
1742
|
+
ignored(`belongs to job "${from.job}"`);
|
|
1743
|
+
return null;
|
|
1744
|
+
}
|
|
1745
|
+
if (from.status === "ok" || from.status === "failed") {
|
|
1746
|
+
ignored(`was already finished as ${from.status}`);
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1749
|
+
const failed = typeof outcome === "object" && outcome !== null && "error" in outcome;
|
|
1750
|
+
const result = typeof outcome === "string" ? outcome : failed ? void 0 : outcome?.result;
|
|
1751
|
+
const error = failed ? outcome.error : void 0;
|
|
1752
|
+
const finishedAt = self.now();
|
|
1753
|
+
const added = recorder.output() ?? (typeof result === "string" ? capOutput(result) : null);
|
|
1754
|
+
const run = {
|
|
1755
|
+
...from,
|
|
1756
|
+
status: "running",
|
|
1757
|
+
finishedAt,
|
|
1758
|
+
durationMs: Math.max(0, finishedAt - from.startedAt),
|
|
1759
|
+
error: null,
|
|
1760
|
+
output: joinOutput(from.output, added),
|
|
1761
|
+
metrics: { ...from.metrics, ...recorder.metrics() }
|
|
1762
|
+
};
|
|
1763
|
+
const expectText = joinLines(head, joinLines(from.output, recorder.expectText() ?? (typeof result === "string" ? result : null)));
|
|
1764
|
+
self.conclude(definition, run, result, error, failed, expectText);
|
|
1765
|
+
let why;
|
|
1766
|
+
try {
|
|
1767
|
+
why = await self.recordFinish(definition, run, recorded, finishedAt);
|
|
1768
|
+
} catch (e) {
|
|
1769
|
+
return retryable(e);
|
|
1770
|
+
}
|
|
1771
|
+
if (why) {
|
|
1772
|
+
ignored(why);
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
return run;
|
|
1776
|
+
});
|
|
1777
|
+
};
|
|
1778
|
+
return {
|
|
1779
|
+
id,
|
|
1780
|
+
job: name,
|
|
1781
|
+
startedAt: base?.startedAt ?? null,
|
|
1782
|
+
get active() {
|
|
1783
|
+
return !finished;
|
|
1784
|
+
},
|
|
1785
|
+
log: (...parts) => recorder.context.log(...parts),
|
|
1786
|
+
metric: (metric, value) => recorder.context.metric(metric, value),
|
|
1787
|
+
metrics: (values) => recorder.context.metrics(values),
|
|
1788
|
+
flush: () => inTurn(async () => {
|
|
1789
|
+
if (finished || !recorded) return;
|
|
1790
|
+
const lines = recorder.output();
|
|
1791
|
+
const metrics = recorder.metrics();
|
|
1792
|
+
if (lines === null && Object.keys(metrics).length === 0) return;
|
|
1793
|
+
const taken = recorder;
|
|
1794
|
+
recorder = fresh();
|
|
1795
|
+
const putBack = () => {
|
|
1796
|
+
const later = recorder;
|
|
1797
|
+
recorder = fresh();
|
|
1798
|
+
for (const text of [taken.expectText(), later.expectText()]) if (text !== null) recorder.context.log(text);
|
|
1799
|
+
recorder.context.metrics({ ...taken.metrics(), ...later.metrics() });
|
|
1800
|
+
};
|
|
1801
|
+
try {
|
|
1802
|
+
const stored = await self.store.getRun(id);
|
|
1803
|
+
if (!stored || stored.status !== "running") return putBack();
|
|
1804
|
+
if (stored.job !== name) {
|
|
1805
|
+
putBack();
|
|
1806
|
+
self.report(new Error(`run ${id} of ${name} belongs to job "${stored.job}"; ignored`), `flushing ${name}`);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const output = lines === null ? stored.output : joinOutput(stored.output, stripNul(self.redact(lines)));
|
|
1810
|
+
if (!await self.writeRunIf({ ...stored, output, metrics: { ...stored.metrics, ...metrics } }, ["running"])) return putBack();
|
|
1811
|
+
const text = taken.expectText();
|
|
1812
|
+
if (text !== null && (head === null || head.length < OUTPUT_CAP)) head = (joinLines(head, text) ?? "").slice(0, OUTPUT_CAP);
|
|
1813
|
+
} catch (e) {
|
|
1814
|
+
putBack();
|
|
1815
|
+
self.report(e, `flushing ${name}`);
|
|
1816
|
+
}
|
|
1817
|
+
}),
|
|
1818
|
+
finish,
|
|
1819
|
+
fail: (error) => finish({ error })
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Record a run that happened outside this process, for a Source. Its job
|
|
1824
|
+
* must be declared with job() first. Runs are keyed by id: a new one is
|
|
1825
|
+
* inserted, a stored one still running (or marked timeout by a check) is
|
|
1826
|
+
* finished when this one is not running, and anything else is left alone,
|
|
1827
|
+
* so recording the same run twice changes nothing. Finishing is
|
|
1828
|
+
* conditional (see Store.updateRunIf): when two processes record the same
|
|
1829
|
+
* finish, only the one whose write lands evaluates it, and the other
|
|
1830
|
+
* reports it as already finished. A stored run of another job is left
|
|
1831
|
+
* alone and reported. A finished run is judged as if it had been wrapped
|
|
1832
|
+
* here (expect, failures, duration, budgets) and its output and error are
|
|
1833
|
+
* redacted the same way; one finishing after a check marked it timeout is
|
|
1834
|
+
* judged only when it succeeded, as RunHandle.finish() does. Returns the
|
|
1835
|
+
* alerts it sent.
|
|
1836
|
+
*/
|
|
1837
|
+
async recordRun(input, options = {}) {
|
|
1838
|
+
const declared = this.definitions.get(input.job);
|
|
1839
|
+
if (!declared) throw new Error(`recordRun: job "${input.job}" is not declared; call job() first`);
|
|
1840
|
+
await this.sync(declared);
|
|
1841
|
+
const run = { ...input, metrics: { ...input.metrics } };
|
|
1842
|
+
if (run.status === "ok") {
|
|
1843
|
+
const unmet = checkExpectation(declared.expect, run.output);
|
|
1844
|
+
if (unmet) {
|
|
1845
|
+
run.status = "failed";
|
|
1846
|
+
run.error = unmet;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
if (run.output !== null) run.output = stripNul(this.redact(capOutput(run.output)));
|
|
1850
|
+
if (run.error !== null) run.error = stripNul(this.redact(capOutput(run.error)));
|
|
1851
|
+
const evaluate = options.evaluate !== false;
|
|
1852
|
+
const definition = toStored(declared);
|
|
1853
|
+
const stored = await this.store.getRun(run.id);
|
|
1854
|
+
if (stored) return this.recordOver(definition, stored, run, evaluate);
|
|
1855
|
+
try {
|
|
1856
|
+
await this.store.insertRun(run);
|
|
1857
|
+
} catch (e) {
|
|
1858
|
+
const again = await this.store.getRun(run.id).catch(() => null);
|
|
1859
|
+
if (again) return this.recordOver(definition, again, run, evaluate);
|
|
1860
|
+
throw e;
|
|
1490
1861
|
}
|
|
1862
|
+
if (!evaluate) return [];
|
|
1863
|
+
await this.updateState(run.job, (before) => ({ state: onRunStart(before), result: void 0 }));
|
|
1864
|
+
if (run.status === "running") return [];
|
|
1865
|
+
return this.finishRun(definition, run, this.now());
|
|
1866
|
+
}
|
|
1867
|
+
/** recordRun() for a run already stored. */
|
|
1868
|
+
async recordOver(definition, stored, run, evaluate) {
|
|
1869
|
+
if (stored.job !== run.job) {
|
|
1870
|
+
this.report(new Error(`run ${run.id} of ${run.job} belongs to job "${stored.job}"; ignored`), `recording ${run.job}`);
|
|
1871
|
+
return [];
|
|
1872
|
+
}
|
|
1873
|
+
if (stored.status !== "running" && stored.status !== "timeout" || run.status === "running") return [];
|
|
1874
|
+
const claim = await this.claimFinish(run);
|
|
1875
|
+
if ("ignored" in claim) {
|
|
1876
|
+
this.report(new Error(`run ${run.id} of ${run.job} ${claim.ignored}; ignored`), `recording ${run.job}`);
|
|
1877
|
+
return [];
|
|
1878
|
+
}
|
|
1879
|
+
if (!evaluate || claim.lateAfterTimeout && run.status !== "ok") return [];
|
|
1880
|
+
return this.finishRun(definition, run, this.now());
|
|
1491
1881
|
}
|
|
1492
1882
|
/**
|
|
1493
|
-
*
|
|
1494
|
-
* against the job's state and send what that produces. Never throws.
|
|
1883
|
+
* Evaluate a finished run (ok, failed, or timed out by a check), already
|
|
1884
|
+
* written, against the job's state and send what that produces. Never throws.
|
|
1495
1885
|
*/
|
|
1496
|
-
async finishRun(definition, run, now
|
|
1886
|
+
async finishRun(definition, run, now) {
|
|
1497
1887
|
let drafts;
|
|
1498
1888
|
try {
|
|
1499
|
-
if (write) await this.store.updateRun(run);
|
|
1500
1889
|
let history = null;
|
|
1501
1890
|
({ result: drafts } = await this.updateState(run.job, async (previous) => {
|
|
1502
1891
|
history ??= await this.history(run);
|
|
@@ -1537,9 +1926,16 @@ var CronWatch = class {
|
|
|
1537
1926
|
}
|
|
1538
1927
|
async runCheck() {
|
|
1539
1928
|
await this.ensureReady();
|
|
1929
|
+
const alerts = [];
|
|
1930
|
+
for (const source of this.sources) {
|
|
1931
|
+
try {
|
|
1932
|
+
alerts.push(...await source.sync(this) ?? []);
|
|
1933
|
+
} catch (e) {
|
|
1934
|
+
this.onError(e, `source ${source.name}`);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1540
1937
|
for (const definition of this.definitions.values()) await this.sync(definition);
|
|
1541
1938
|
const now = this.now();
|
|
1542
|
-
const alerts = [];
|
|
1543
1939
|
for (const run of await this.store.runningRuns()) {
|
|
1544
1940
|
try {
|
|
1545
1941
|
const declared = this.definitions.get(run.job);
|
|
@@ -1550,7 +1946,8 @@ var CronWatch = class {
|
|
|
1550
1946
|
run.finishedAt = now;
|
|
1551
1947
|
run.durationMs = now - run.startedAt;
|
|
1552
1948
|
run.error = `Still running after ${formatDuration(timeout)}; marked as timed out`;
|
|
1553
|
-
|
|
1949
|
+
if (!await this.writeRunIf(run, ["running"])) continue;
|
|
1950
|
+
alerts.push(...await this.finishRun(definition, run, now));
|
|
1554
1951
|
} catch (e) {
|
|
1555
1952
|
this.onError(e, `checking ${run.job}`);
|
|
1556
1953
|
}
|
|
@@ -1668,7 +2065,11 @@ var CronWatch = class {
|
|
|
1668
2065
|
routes(options) {
|
|
1669
2066
|
return createRoutes(this, options);
|
|
1670
2067
|
}
|
|
1671
|
-
/**
|
|
2068
|
+
/**
|
|
2069
|
+
* Check on an interval, for long-running servers. Default every minute.
|
|
2070
|
+
* Not for serverless functions or Cloudflare Workers, where nothing runs
|
|
2071
|
+
* between requests: call check() from a cron there instead.
|
|
2072
|
+
*/
|
|
1672
2073
|
start(every = "1m") {
|
|
1673
2074
|
if (this.timer) return;
|
|
1674
2075
|
const ms = Math.max(5e3, parseDuration(every, "check interval"));
|
|
@@ -1777,7 +2178,7 @@ var CronWatch = class {
|
|
|
1777
2178
|
const results = await Promise.all(
|
|
1778
2179
|
this.alerts.map(async (channel) => {
|
|
1779
2180
|
try {
|
|
1780
|
-
await withTimeout(channel.send(alert), CHANNEL_TIMEOUT_MS);
|
|
2181
|
+
await withTimeout(channel.send(alert, { onError: (e) => this.report(e, `alert channel ${channel.name}`) }), CHANNEL_TIMEOUT_MS);
|
|
1781
2182
|
return true;
|
|
1782
2183
|
} catch (e) {
|
|
1783
2184
|
this.onError(e, `alert channel ${channel.name}`);
|
|
@@ -1801,8 +2202,24 @@ var CronWatch = class {
|
|
|
1801
2202
|
}
|
|
1802
2203
|
}
|
|
1803
2204
|
};
|
|
1804
|
-
|
|
1805
|
-
|
|
2205
|
+
var RESERVED_RUN_ID_PREFIX = "pgcron:";
|
|
2206
|
+
function checkRunId(job, id, method) {
|
|
2207
|
+
if (typeof id !== "string" || id.length === 0 || id.length > 200) {
|
|
2208
|
+
throw new Error(`job "${job}": ${method}() needs a run id of 1 to 200 characters (got ${typeof id === "string" ? `${id.length} characters` : typeof id})`);
|
|
2209
|
+
}
|
|
2210
|
+
if (id.startsWith(RESERVED_RUN_ID_PREFIX)) {
|
|
2211
|
+
throw new Error(`job "${job}": ${method}() cannot take a run id starting with "${RESERVED_RUN_ID_PREFIX}", which the pg_cron source uses for its runs`);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
function joinLines(before, after) {
|
|
2215
|
+
if (before === null || before === "") return after;
|
|
2216
|
+
if (after === null) return before;
|
|
2217
|
+
return `${before}
|
|
2218
|
+
${after}`;
|
|
2219
|
+
}
|
|
2220
|
+
function joinOutput(before, after) {
|
|
2221
|
+
const joined = joinLines(before, after);
|
|
2222
|
+
return joined === null ? null : capOutput(joined);
|
|
1806
2223
|
}
|
|
1807
2224
|
function clampLimit(limit, fallback, min) {
|
|
1808
2225
|
const n = typeof limit === "number" && Number.isFinite(limit) ? Math.trunc(limit) : fallback;
|