@cronwatch/sdk 0.12.4 → 0.13.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 +3 -3
- package/dist/anthropic.cjs +2 -2
- package/dist/anthropic.cjs.map +1 -1
- package/dist/anthropic.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/anthropic.js +2 -2
- package/dist/anthropic.js.map +1 -1
- package/dist/bugsnag.cjs +7 -0
- package/dist/bugsnag.cjs.map +1 -1
- package/dist/bugsnag.d.cts +1 -1
- package/dist/bugsnag.d.ts +1 -1
- package/dist/bugsnag.js +7 -0
- package/dist/bugsnag.js.map +1 -1
- package/dist/{client-BkPMRkDq.d.cts → client-DAJH7YiG.d.ts} +41 -20
- package/dist/{client-DySsV5T0.d.ts → client-z1LdR_m2.d.cts} +41 -20
- package/dist/d1.cjs +1 -1
- package/dist/d1.cjs.map +1 -1
- package/dist/d1.d.cts +4 -4
- package/dist/d1.d.ts +4 -4
- package/dist/d1.js +1 -1
- package/dist/d1.js.map +1 -1
- package/dist/datadog.cjs +7 -0
- package/dist/datadog.cjs.map +1 -1
- package/dist/datadog.d.cts +2 -2
- package/dist/datadog.d.ts +2 -2
- package/dist/datadog.js +7 -0
- package/dist/datadog.js.map +1 -1
- package/dist/discord.cjs +25 -1
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +5 -5
- package/dist/discord.d.ts +5 -5
- package/dist/discord.js +25 -1
- package/dist/discord.js.map +1 -1
- package/dist/{email-CD6NO8CV.d.cts → email-CCUzlLe9.d.cts} +3 -3
- package/dist/{email-Bx3ehK0O.d.ts → email-CFtDqlvZ.d.ts} +3 -3
- package/dist/honeybadger.cjs +7 -0
- package/dist/honeybadger.cjs.map +1 -1
- package/dist/honeybadger.d.cts +1 -1
- package/dist/honeybadger.d.ts +1 -1
- package/dist/honeybadger.js +7 -0
- package/dist/honeybadger.js.map +1 -1
- package/dist/index.cjs +206 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +206 -58
- package/dist/index.js.map +1 -1
- package/dist/mailgun.cjs +7 -0
- package/dist/mailgun.cjs.map +1 -1
- package/dist/mailgun.d.cts +2 -2
- package/dist/mailgun.d.ts +2 -2
- package/dist/mailgun.js +7 -0
- package/dist/mailgun.js.map +1 -1
- package/dist/newrelic.cjs +7 -0
- package/dist/newrelic.cjs.map +1 -1
- package/dist/newrelic.d.cts +1 -1
- package/dist/newrelic.d.ts +1 -1
- package/dist/newrelic.js +7 -0
- package/dist/newrelic.js.map +1 -1
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +3 -3
- package/dist/node.d.ts +3 -3
- package/dist/node.js.map +1 -1
- package/dist/pg-cron.cjs.map +1 -1
- package/dist/pg-cron.d.cts +10 -10
- package/dist/pg-cron.d.ts +10 -10
- package/dist/pg-cron.js.map +1 -1
- package/dist/postgres.cjs +8 -3
- package/dist/postgres.cjs.map +1 -1
- package/dist/postgres.d.cts +3 -3
- package/dist/postgres.d.ts +3 -3
- package/dist/postgres.js +8 -3
- package/dist/postgres.js.map +1 -1
- package/dist/postmark.cjs +7 -0
- package/dist/postmark.cjs.map +1 -1
- package/dist/postmark.d.cts +2 -2
- package/dist/postmark.d.ts +2 -2
- package/dist/postmark.js +7 -0
- package/dist/postmark.js.map +1 -1
- package/dist/resend.cjs +7 -0
- package/dist/resend.cjs.map +1 -1
- package/dist/resend.d.cts +2 -2
- package/dist/resend.d.ts +2 -2
- package/dist/resend.js +7 -0
- package/dist/resend.js.map +1 -1
- package/dist/rollbar.cjs +7 -0
- package/dist/rollbar.cjs.map +1 -1
- package/dist/rollbar.d.cts +1 -1
- package/dist/rollbar.d.ts +1 -1
- package/dist/rollbar.js +7 -0
- package/dist/rollbar.js.map +1 -1
- package/dist/sendgrid.cjs +7 -0
- package/dist/sendgrid.cjs.map +1 -1
- package/dist/sendgrid.d.cts +2 -2
- package/dist/sendgrid.d.ts +2 -2
- package/dist/sendgrid.js +7 -0
- package/dist/sendgrid.js.map +1 -1
- package/dist/sentry.cjs +7 -0
- package/dist/sentry.cjs.map +1 -1
- package/dist/sentry.d.cts +2 -2
- package/dist/sentry.d.ts +2 -2
- package/dist/sentry.js +7 -0
- package/dist/sentry.js.map +1 -1
- package/dist/ses.cjs +7 -0
- package/dist/ses.cjs.map +1 -1
- package/dist/ses.d.cts +2 -2
- package/dist/ses.d.ts +2 -2
- package/dist/ses.js +7 -0
- package/dist/ses.js.map +1 -1
- package/dist/slack.cjs +32 -1
- 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 +32 -1
- package/dist/slack.js.map +1 -1
- package/dist/sqlite.cjs +1 -1
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +2 -2
- package/dist/sqlite.d.ts +2 -2
- package/dist/sqlite.js +1 -1
- package/dist/sqlite.js.map +1 -1
- package/dist/twilio.cjs +7 -0
- package/dist/twilio.cjs.map +1 -1
- package/dist/twilio.d.cts +5 -5
- package/dist/twilio.d.ts +5 -5
- package/dist/twilio.js +7 -0
- package/dist/twilio.js.map +1 -1
- package/dist/{types-Bm972Uqj.d.cts → types-D6t0Z846.d.cts} +15 -7
- package/dist/{types-Bm972Uqj.d.ts → types-D6t0Z846.d.ts} +15 -7
- package/dist/webhook.cjs +11 -1
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +4 -3
- package/dist/webhook.d.ts +4 -3
- package/dist/webhook.js +11 -1
- package/dist/webhook.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,7 @@ function parseDuration(value, label = "duration") {
|
|
|
30
30
|
consumed += match[0];
|
|
31
31
|
}
|
|
32
32
|
if (consumed.replace(/\s+/g, "") !== text.replace(/\s+/g, "")) {
|
|
33
|
-
throw new Error(`${label} "${value}" is not a duration like "15m", "1h30m" or "90s"`);
|
|
33
|
+
throw new Error(`${label} "${value}" is not a duration like "15m", "1h30m", or "90s"`);
|
|
34
34
|
}
|
|
35
35
|
return Math.round(total);
|
|
36
36
|
}
|
|
@@ -81,27 +81,31 @@ function readEnv(name) {
|
|
|
81
81
|
return typeof process === "undefined" || !process.env ? void 0 : process.env[name];
|
|
82
82
|
}
|
|
83
83
|
function readSecretEnv(name) {
|
|
84
|
-
const value = readEnv(name);
|
|
85
|
-
return value === void 0 || value
|
|
84
|
+
const value = readEnv(name)?.trim();
|
|
85
|
+
return value === void 0 || value === "" ? void 0 : value;
|
|
86
86
|
}
|
|
87
87
|
function secretOption(value, what) {
|
|
88
88
|
if (value === void 0 || value === null) return value;
|
|
89
89
|
if (typeof value !== "string") {
|
|
90
90
|
throw new TypeError(`${what} must be a string, or null to opt out, not ${Array.isArray(value) ? "an array" : typeof value}`);
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
const trimmed = value.trim();
|
|
93
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
93
94
|
}
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
for (const value of [process.env.CRONWATCH_ENV, process.env.APP_ENV, process.env.NODE_ENV]) {
|
|
95
|
+
function environmentOf(values) {
|
|
96
|
+
for (const value of values) {
|
|
97
97
|
const name = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
98
98
|
if (name === "") continue;
|
|
99
99
|
if (name === "prod") return "production";
|
|
100
|
-
if (name === "dev" || name === "local"
|
|
100
|
+
if (name === "dev" || name === "local") return "development";
|
|
101
101
|
return name;
|
|
102
102
|
}
|
|
103
103
|
return "";
|
|
104
104
|
}
|
|
105
|
+
function environment() {
|
|
106
|
+
if (typeof process === "undefined" || !process.env) return "";
|
|
107
|
+
return environmentOf([process.env.CRONWATCH_ENV, process.env.APP_ENV, process.env.NODE_ENV]);
|
|
108
|
+
}
|
|
105
109
|
function isDevelopment() {
|
|
106
110
|
return environment() === "development";
|
|
107
111
|
}
|
|
@@ -328,7 +332,7 @@ function isObject(value) {
|
|
|
328
332
|
}
|
|
329
333
|
function normalizeState(state, job) {
|
|
330
334
|
if (!isObject(state)) return emptyState(job);
|
|
331
|
-
const { sending, underFloor, ...rest } = state;
|
|
335
|
+
const { sending, underFloor, pendingRuns, ...rest } = state;
|
|
332
336
|
const open = isObject(state.open) ? Object.fromEntries(Object.entries(state.open).filter(([, at]) => typeof at === "number")) : {};
|
|
333
337
|
const number = (value) => typeof value === "number" ? value : null;
|
|
334
338
|
const list = (value) => Array.isArray(value) ? value : [];
|
|
@@ -342,7 +346,8 @@ function normalizeState(state, job) {
|
|
|
342
346
|
pendingRecovery: list(state.pendingRecovery).filter((c) => typeof c === "string"),
|
|
343
347
|
undelivered: list(state.undelivered).filter((alert) => isObject(alert)),
|
|
344
348
|
...Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {},
|
|
345
|
-
...Array.isArray(underFloor) && underFloor.some((m) => typeof m === "string") ? { underFloor: underFloor.filter((m) => typeof m === "string") } : {}
|
|
349
|
+
...Array.isArray(underFloor) && underFloor.some((m) => typeof m === "string") ? { underFloor: underFloor.filter((m) => typeof m === "string") } : {},
|
|
350
|
+
...Array.isArray(pendingRuns) && pendingRuns.some((id) => typeof id === "string") ? { pendingRuns: pendingRuns.filter((id) => typeof id === "string") } : {}
|
|
346
351
|
};
|
|
347
352
|
}
|
|
348
353
|
var MAX_UNDELIVERED = 20;
|
|
@@ -392,6 +397,34 @@ function recordSent(state, delivered, failed, stale, now) {
|
|
|
392
397
|
if (delivered.length > 0) next.lastAlertAt = now;
|
|
393
398
|
return queueUndelivered(next, failed);
|
|
394
399
|
}
|
|
400
|
+
function claimUndelivered(state, now, until) {
|
|
401
|
+
const next = cloneState(state);
|
|
402
|
+
const queue = next.undelivered;
|
|
403
|
+
const stale = queue.filter((alert) => staleAlert(alert, next));
|
|
404
|
+
const silenced = isSilenced(next, now);
|
|
405
|
+
const room = Math.max(0, MAX_UNDELIVERED - (next.sending ?? []).length);
|
|
406
|
+
const claimed = queue.filter((alert) => !stale.includes(alert) && (!silenced || alert.type === "recovered")).slice(0, room);
|
|
407
|
+
if (stale.length === 0 && claimed.length === 0) return { state, claimed, stale };
|
|
408
|
+
next.undelivered = queue.filter((alert) => !stale.includes(alert) && !claimed.includes(alert));
|
|
409
|
+
if (claimed.length > 0) next.sending = [...next.sending ?? [], ...claimed.map((alert) => ({ until, alert }))];
|
|
410
|
+
return { state: next, claimed, stale };
|
|
411
|
+
}
|
|
412
|
+
function holdRun(state, runId) {
|
|
413
|
+
const next = cloneState(state);
|
|
414
|
+
const held = (next.pendingRuns ?? []).filter((id) => id !== runId);
|
|
415
|
+
held.push(runId);
|
|
416
|
+
next.pendingRuns = held.slice(-100);
|
|
417
|
+
return next;
|
|
418
|
+
}
|
|
419
|
+
function releaseRun(state, runId) {
|
|
420
|
+
const held = state.pendingRuns ?? [];
|
|
421
|
+
if (!held.includes(runId)) return state;
|
|
422
|
+
const next = cloneState(state);
|
|
423
|
+
const left = held.filter((id) => id !== runId);
|
|
424
|
+
if (left.length > 0) next.pendingRuns = left;
|
|
425
|
+
else delete next.pendingRuns;
|
|
426
|
+
return next;
|
|
427
|
+
}
|
|
395
428
|
function cloneState(state) {
|
|
396
429
|
return normalizeState(state, state.job);
|
|
397
430
|
}
|
|
@@ -572,7 +605,7 @@ function isSilenced(state, now) {
|
|
|
572
605
|
}
|
|
573
606
|
function applySilence(previous, evaluation, now) {
|
|
574
607
|
if (!isSilenced(previous, now)) return evaluation;
|
|
575
|
-
return { state: muteOpens(previous, evaluation.state), alerts:
|
|
608
|
+
return { state: muteOpens(previous, evaluation.state), alerts: evaluation.alerts.filter((alert) => alert.type === "recovered") };
|
|
576
609
|
}
|
|
577
610
|
function staleAlert(alert, state) {
|
|
578
611
|
if (alert.type === "recovered") {
|
|
@@ -636,6 +669,10 @@ function tail(text, n) {
|
|
|
636
669
|
const lines = text.trimEnd().split("\n");
|
|
637
670
|
return lines.slice(Math.max(0, lines.length - n)).join("\n");
|
|
638
671
|
}
|
|
672
|
+
function andList(words2) {
|
|
673
|
+
if (words2.length <= 2) return words2.join(" and ");
|
|
674
|
+
return `${words2.slice(0, -1).join(", ")}, and ${words2[words2.length - 1]}`;
|
|
675
|
+
}
|
|
639
676
|
function errorLine(error) {
|
|
640
677
|
const text = firstLines(error, 4);
|
|
641
678
|
return /^[A-Za-z_$][\w$]*: /.test(text) ? text : `Error: ${text}`;
|
|
@@ -709,7 +746,7 @@ ${out}`);
|
|
|
709
746
|
break;
|
|
710
747
|
}
|
|
711
748
|
title = `${name} recovered`;
|
|
712
|
-
const after2 = draft.details.after.map((c) => c.replace("_", " "))
|
|
749
|
+
const after2 = andList(draft.details.after.map((c) => c.replace("_", " ")));
|
|
713
750
|
lines.push(`A run ${run ? when(run.startedAt, now) : "just now"} succeeded${after2 ? ` after: ${after2}` : ""}.`);
|
|
714
751
|
if (run?.durationMs !== null && run?.durationMs !== void 0) lines.push(`Ran ${formatDuration(run.durationMs)}.`);
|
|
715
752
|
break;
|
|
@@ -725,10 +762,10 @@ function json(body, status = 200, headers = {}) {
|
|
|
725
762
|
headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...headers }
|
|
726
763
|
});
|
|
727
764
|
}
|
|
728
|
-
function constantTimeEqual(
|
|
729
|
-
if (
|
|
730
|
-
let diff =
|
|
731
|
-
for (let i = 0; i <
|
|
765
|
+
function constantTimeEqual(sent, secret) {
|
|
766
|
+
if (secret.length === 0) return sent.length === 0;
|
|
767
|
+
let diff = sent.length ^ secret.length;
|
|
768
|
+
for (let i = 0; i < sent.length; i++) diff |= sent.charCodeAt(i) ^ secret.charCodeAt(i % secret.length);
|
|
732
769
|
return diff === 0;
|
|
733
770
|
}
|
|
734
771
|
|
|
@@ -799,7 +836,7 @@ var SECRET_PATTERNS = [
|
|
|
799
836
|
// Incoming webhook URLs carry their secret in the path.
|
|
800
837
|
[/(\bhooks\.slack\.com\/(?:services|workflows|triggers)\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],
|
|
801
838
|
[/(\bdiscord(?:app)?\.com\/api\/(?:v\d{1,2}\/)?webhooks\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],
|
|
802
|
-
// Well-known token shapes: AWS, GitHub, Slack, Stripe, Anthropic, OpenAI and Google style keys.
|
|
839
|
+
// Well-known token shapes: AWS, GitHub, Slack, Stripe, Anthropic, OpenAI, and Google style keys.
|
|
803
840
|
[/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, REDACTED],
|
|
804
841
|
[/\b(?:gh[pousr]_[A-Za-z0-9]{30,255}|github_pat_[A-Za-z0-9_]{20,255})\b/g, REDACTED],
|
|
805
842
|
[/\bxox[abposr]-[A-Za-z0-9-]{10,255}/g, REDACTED],
|
|
@@ -937,7 +974,7 @@ function manifest(base) {
|
|
|
937
974
|
id: `${base}/`,
|
|
938
975
|
name: "CronWatch",
|
|
939
976
|
short_name: "CronWatch",
|
|
940
|
-
description: "The scheduled jobs of this app: their health, their last day and their runs.",
|
|
977
|
+
description: "The scheduled jobs of this app: their health, their last day, and their runs.",
|
|
941
978
|
start_url: `${base}/`,
|
|
942
979
|
scope: `${base}/`,
|
|
943
980
|
display: "standalone",
|
|
@@ -1041,7 +1078,7 @@ function staticAsset(path, base) {
|
|
|
1041
1078
|
}
|
|
1042
1079
|
|
|
1043
1080
|
// src/version.ts
|
|
1044
|
-
var VERSION = "0.
|
|
1081
|
+
var VERSION = "0.13.0";
|
|
1045
1082
|
var API_VERSION = 1;
|
|
1046
1083
|
|
|
1047
1084
|
// src/routes/timeline.ts
|
|
@@ -1157,11 +1194,11 @@ function laneNote(job, missed, now) {
|
|
|
1157
1194
|
if (job.open.includes("stuck")) return "stuck";
|
|
1158
1195
|
if (job.open.includes("over_budget") && last) {
|
|
1159
1196
|
const over = overCeilings(job);
|
|
1160
|
-
return `went over budget${over.length ? ` on ${over
|
|
1197
|
+
return `went over budget${over.length ? ` on ${andList(over)}` : ""} at ${when2(last.startedAt, now)}`;
|
|
1161
1198
|
}
|
|
1162
1199
|
if (job.open.includes("under_floor") && last) {
|
|
1163
1200
|
const under = underFloors(job);
|
|
1164
|
-
return `fell short${under.length ? ` on ${under
|
|
1201
|
+
return `fell short${under.length ? ` on ${andList(under)}` : ""} at ${when2(last.startedAt, now)}`;
|
|
1165
1202
|
}
|
|
1166
1203
|
if (job.open.includes("slow") && last?.durationMs != null) return `slow: took ${formatDuration(last.durationMs)}`;
|
|
1167
1204
|
if (job.open.includes("failed")) return "failing";
|
|
@@ -1279,7 +1316,7 @@ function legend() {
|
|
|
1279
1316
|
[box("run ok"), "ran"],
|
|
1280
1317
|
[box("run bad"), "failed"],
|
|
1281
1318
|
[box("run timeout"), "timed out"],
|
|
1282
|
-
[box("run warn"), "over budget, under floor or slow"],
|
|
1319
|
+
[box("run warn"), "over budget, under floor, or slow"],
|
|
1283
1320
|
[box("run running"), "running"],
|
|
1284
1321
|
[box("missed"), "missed"]
|
|
1285
1322
|
];
|
|
@@ -1828,23 +1865,63 @@ function stripBase(pathname, base) {
|
|
|
1828
1865
|
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
|
1829
1866
|
return path;
|
|
1830
1867
|
}
|
|
1868
|
+
var MAX_REQUEST_BODY = 1 << 20;
|
|
1869
|
+
var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
|
|
1870
|
+
async function readLimited(request) {
|
|
1871
|
+
const length = Number(request.headers.get("content-length"));
|
|
1872
|
+
if (Number.isFinite(length) && length > MAX_REQUEST_BODY) return TOO_LARGE;
|
|
1873
|
+
if (!request.body) return new Uint8Array(new ArrayBuffer(0));
|
|
1874
|
+
const reader = request.body.getReader();
|
|
1875
|
+
const chunks = [];
|
|
1876
|
+
let size = 0;
|
|
1877
|
+
try {
|
|
1878
|
+
for (; ; ) {
|
|
1879
|
+
const { done, value } = await reader.read();
|
|
1880
|
+
if (done) break;
|
|
1881
|
+
size += value.byteLength;
|
|
1882
|
+
if (size > MAX_REQUEST_BODY) {
|
|
1883
|
+
await reader.cancel().catch(() => {
|
|
1884
|
+
});
|
|
1885
|
+
return TOO_LARGE;
|
|
1886
|
+
}
|
|
1887
|
+
chunks.push(value);
|
|
1888
|
+
}
|
|
1889
|
+
} finally {
|
|
1890
|
+
reader.releaseLock();
|
|
1891
|
+
}
|
|
1892
|
+
const out = new Uint8Array(new ArrayBuffer(size));
|
|
1893
|
+
let at = 0;
|
|
1894
|
+
for (const chunk of chunks) {
|
|
1895
|
+
out.set(chunk, at);
|
|
1896
|
+
at += chunk.byteLength;
|
|
1897
|
+
}
|
|
1898
|
+
return out;
|
|
1899
|
+
}
|
|
1831
1900
|
async function readBody(request) {
|
|
1832
1901
|
const type = request.headers.get("content-type") ?? "";
|
|
1902
|
+
const json2 = type.includes("application/json");
|
|
1903
|
+
const form = type.includes("application/x-www-form-urlencoded") || type.includes("multipart/form-data");
|
|
1904
|
+
if (!json2 && !form) return {};
|
|
1905
|
+
let bytes;
|
|
1906
|
+
try {
|
|
1907
|
+
bytes = await readLimited(request);
|
|
1908
|
+
} catch {
|
|
1909
|
+
return {};
|
|
1910
|
+
}
|
|
1911
|
+
if (bytes === TOO_LARGE) return TOO_LARGE;
|
|
1912
|
+
const copy = new Request(request.url, { method: "POST", headers: { "content-type": type }, body: bytes });
|
|
1833
1913
|
try {
|
|
1834
|
-
if (
|
|
1835
|
-
const data = await
|
|
1914
|
+
if (json2) {
|
|
1915
|
+
const data = await copy.json();
|
|
1836
1916
|
return typeof data === "object" && data !== null ? Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v)])) : {};
|
|
1837
1917
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
return out;
|
|
1843
|
-
}
|
|
1918
|
+
const fields = await copy.formData();
|
|
1919
|
+
const out = {};
|
|
1920
|
+
for (const [k, v] of fields.entries()) out[k] = String(v);
|
|
1921
|
+
return out;
|
|
1844
1922
|
} catch {
|
|
1845
1923
|
return {};
|
|
1846
1924
|
}
|
|
1847
|
-
return {};
|
|
1848
1925
|
}
|
|
1849
1926
|
function crossSite(request, publicOrigin) {
|
|
1850
1927
|
const origin = request.headers.get("origin");
|
|
@@ -1908,7 +1985,8 @@ function buildRoutes(cw, options = {}) {
|
|
|
1908
1985
|
};
|
|
1909
1986
|
const signInPage = () => generated ? 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, or enter the token from it below, and this browser stays signed in.", base, true), 401) : html(messagePage("Sign in", "Enter your CRONWATCH_TOKEN and this browser stays signed in.", base, true), 401);
|
|
1910
1987
|
if (token && method === "POST" && path === "/signin") {
|
|
1911
|
-
const
|
|
1988
|
+
const body = await readBody(request);
|
|
1989
|
+
const sent = body === TOO_LARGE ? void 0 : body.token;
|
|
1912
1990
|
if (sent === void 0 || !constantTimeEqual(sent, token)) return signInPage();
|
|
1913
1991
|
return redirect(signInReturn(request.headers.get("referer"), publicOrigin, base), { "set-cookie": await signInCookie() });
|
|
1914
1992
|
}
|
|
@@ -1924,7 +2002,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
1924
2002
|
}
|
|
1925
2003
|
if (query !== null) {
|
|
1926
2004
|
url.searchParams.delete("token");
|
|
1927
|
-
return redirect(url.pathname + (url.search || ""), { "set-cookie": await signInCookie() });
|
|
2005
|
+
return redirect(url.pathname.replace(/^\/+/, "/") + (url.search || ""), { "set-cookie": await signInCookie() });
|
|
1928
2006
|
}
|
|
1929
2007
|
}
|
|
1930
2008
|
const redirectBack = () => {
|
|
@@ -1963,9 +2041,11 @@ function buildRoutes(cw, options = {}) {
|
|
|
1963
2041
|
if (parts[2] !== "silence" && parts[2] !== "unsilence") return html(messagePage("Not found", path, base), 404);
|
|
1964
2042
|
if (!await cw.jobSummary(name)) return html(messagePage("No such job", `${name} is not in the store.`, base), 404);
|
|
1965
2043
|
if (parts[2] === "silence") {
|
|
2044
|
+
const body = await readBody(request);
|
|
2045
|
+
if (body === TOO_LARGE) return html(messagePage("Not silenced", "The request was too large.", base), 413);
|
|
1966
2046
|
let duration;
|
|
1967
2047
|
try {
|
|
1968
|
-
duration = silenceDuration(
|
|
2048
|
+
duration = silenceDuration(body.for);
|
|
1969
2049
|
} catch (e) {
|
|
1970
2050
|
return html(messagePage("Not silenced", e.message, base), 400);
|
|
1971
2051
|
}
|
|
@@ -1999,6 +2079,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
1999
2079
|
if (!await cw.jobSummary(name)) return api({ ok: false, error: "No such job" }, 404);
|
|
2000
2080
|
if (rest[2] === "silence") {
|
|
2001
2081
|
const body = await readBody(request);
|
|
2082
|
+
if (body === TOO_LARGE) return api({ ok: false, error: "Request body too large" }, 413);
|
|
2002
2083
|
let duration;
|
|
2003
2084
|
try {
|
|
2004
2085
|
duration = silenceDuration(body.for ?? url.searchParams.get("for"));
|
|
@@ -2225,7 +2306,7 @@ function validateDefinition(def) {
|
|
|
2225
2306
|
}
|
|
2226
2307
|
}
|
|
2227
2308
|
if (def.expect !== void 0 && typeof def.expect !== "string" && !(def.expect instanceof RegExp) && typeof def.expect !== "function") {
|
|
2228
|
-
throw new Error(`job "${name}": expect must be a string, a RegExp or a function`);
|
|
2309
|
+
throw new Error(`job "${name}": expect must be a string, a RegExp, or a function`);
|
|
2229
2310
|
}
|
|
2230
2311
|
}
|
|
2231
2312
|
function sameState(a, b) {
|
|
@@ -2248,6 +2329,11 @@ var Cronwatch = class {
|
|
|
2248
2329
|
deferDelivery;
|
|
2249
2330
|
defaults;
|
|
2250
2331
|
definitions = /* @__PURE__ */ new Map();
|
|
2332
|
+
/**
|
|
2333
|
+
* Declarations forgotten here (forget()), kept so that run() or resumeRun()
|
|
2334
|
+
* by name declares the job again as it was written, not bare.
|
|
2335
|
+
*/
|
|
2336
|
+
forgotten = /* @__PURE__ */ new Map();
|
|
2251
2337
|
synced = /* @__PURE__ */ new Set();
|
|
2252
2338
|
/** The tail of each job's queue of state updates. See serial(). */
|
|
2253
2339
|
queues = /* @__PURE__ */ new Map();
|
|
@@ -2297,18 +2383,31 @@ var Cronwatch = class {
|
|
|
2297
2383
|
/** Declare a job. Call it once, at module level, and keep the handle. */
|
|
2298
2384
|
job(name, options = {}) {
|
|
2299
2385
|
if (!NAME_RE.test(name)) {
|
|
2300
|
-
throw new Error(`job name "${name}" must be 1 to 120 characters of letters, digits, ".", "_", ":" or "-"`);
|
|
2386
|
+
throw new Error(`job name "${name}" must be 1 to 120 characters of letters, digits, ".", "_", ":", or "-"`);
|
|
2301
2387
|
}
|
|
2302
2388
|
const definition = { ...this.defaults, ...options, name };
|
|
2303
2389
|
validateDefinition(definition);
|
|
2304
2390
|
this.definitions.set(name, definition);
|
|
2391
|
+
this.forgotten.delete(name);
|
|
2305
2392
|
this.synced.delete(name);
|
|
2306
2393
|
return this.handle(definition);
|
|
2307
2394
|
}
|
|
2395
|
+
/** The declaration of `name`: the standing one, else one forgotten here, declared again as it was. */
|
|
2396
|
+
declared(name) {
|
|
2397
|
+
const standing = this.definitions.get(name);
|
|
2398
|
+
if (standing) return standing;
|
|
2399
|
+
const forgotten = this.forgotten.get(name);
|
|
2400
|
+
if (!forgotten) return void 0;
|
|
2401
|
+
this.forgotten.delete(name);
|
|
2402
|
+
this.definitions.set(name, forgotten);
|
|
2403
|
+
this.synced.delete(name);
|
|
2404
|
+
return forgotten;
|
|
2405
|
+
}
|
|
2308
2406
|
run(name, optionsOrFn, maybeFn) {
|
|
2309
2407
|
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
2310
2408
|
const options = typeof optionsOrFn === "function" ? void 0 : optionsOrFn;
|
|
2311
|
-
const
|
|
2409
|
+
const declared = options ? void 0 : this.declared(name);
|
|
2410
|
+
const handle = declared ? this.handle(declared) : this.job(name, options);
|
|
2312
2411
|
return handle.run(fn);
|
|
2313
2412
|
}
|
|
2314
2413
|
/** The definitions declared in this process. */
|
|
@@ -2317,7 +2416,7 @@ var Cronwatch = class {
|
|
|
2317
2416
|
}
|
|
2318
2417
|
/** A handle on a run started elsewhere, as job(name).resume(runId). The job must be declared in this process. */
|
|
2319
2418
|
resumeRun(name, runId) {
|
|
2320
|
-
const definition = this.
|
|
2419
|
+
const definition = this.declared(name);
|
|
2321
2420
|
if (!definition) return Promise.reject(new Error(`resumeRun: job "${name}" is not declared; call job() first`));
|
|
2322
2421
|
return this.resumeHandle(definition, runId);
|
|
2323
2422
|
}
|
|
@@ -2533,7 +2632,7 @@ var Cronwatch = class {
|
|
|
2533
2632
|
} catch (e) {
|
|
2534
2633
|
this.onError(e, `recording ${name}`);
|
|
2535
2634
|
}
|
|
2536
|
-
const started = recorded ? this.updateState(name, (before) => ({ state: onRunStart(before), result: void 0 })).then(() => {
|
|
2635
|
+
const started = recorded ? this.updateState(name, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 })).then(() => {
|
|
2537
2636
|
}, (e) => this.onError(e, `starting ${name}`)) : Promise.resolve();
|
|
2538
2637
|
const recorder = createRecorder(run);
|
|
2539
2638
|
const clearTimer = after(timeoutMs(definition), () => recorder.abort());
|
|
@@ -2689,7 +2788,7 @@ ${id}`;
|
|
|
2689
2788
|
this.onError(e, `recording ${name}`);
|
|
2690
2789
|
}
|
|
2691
2790
|
if (recorded) {
|
|
2692
|
-
await this.updateState(name, (before) => ({ state: onRunStart(before), result: void 0 })).catch((e) => this.onError(e, `starting ${name}`));
|
|
2791
|
+
await this.updateState(name, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 })).catch((e) => this.onError(e, `starting ${name}`));
|
|
2693
2792
|
}
|
|
2694
2793
|
return this.runHandle(definition, run.id, run, recorded, null);
|
|
2695
2794
|
}
|
|
@@ -2902,7 +3001,7 @@ ${id}`;
|
|
|
2902
3001
|
throw e;
|
|
2903
3002
|
}
|
|
2904
3003
|
if (!evaluate) return [];
|
|
2905
|
-
await this.updateState(run.job, (before) => ({ state: onRunStart(before), result: void 0 }));
|
|
3004
|
+
await this.updateState(run.job, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 }));
|
|
2906
3005
|
if (run.status === "running") return [];
|
|
2907
3006
|
return this.finishRun(definition, run, this.now());
|
|
2908
3007
|
}
|
|
@@ -2918,7 +3017,11 @@ ${id}`;
|
|
|
2918
3017
|
this.report(new Error(`run ${run.id} of ${run.job} ${claim.ignored}; ignored`), `recording ${run.job}`);
|
|
2919
3018
|
return [];
|
|
2920
3019
|
}
|
|
2921
|
-
if (!evaluate
|
|
3020
|
+
if (!evaluate) {
|
|
3021
|
+
await this.updateState(run.job, (current) => ({ state: releaseRun(current, run.id), result: void 0 })).catch((e) => this.onError(e, `recording ${run.job}`));
|
|
3022
|
+
return [];
|
|
3023
|
+
}
|
|
3024
|
+
if (claim.lateAfterTimeout && run.status !== "ok") return [];
|
|
2922
3025
|
return this.finishRun(definition, run, this.now());
|
|
2923
3026
|
}
|
|
2924
3027
|
/**
|
|
@@ -2932,7 +3035,8 @@ ${id}`;
|
|
|
2932
3035
|
let history = null;
|
|
2933
3036
|
({ result: held } = await this.updateState(run.job, async (previous) => {
|
|
2934
3037
|
history ??= await this.history(run);
|
|
2935
|
-
|
|
3038
|
+
const before = releaseRun(previous, run.id);
|
|
3039
|
+
return this.outbox(applySilence(before, onRunFinish(definition, run, before, history, now), now), definition, now);
|
|
2936
3040
|
}));
|
|
2937
3041
|
} catch (e) {
|
|
2938
3042
|
this.onError(e, `evaluating ${run.job}`);
|
|
@@ -3037,6 +3141,7 @@ ${id}`;
|
|
|
3037
3141
|
this.reportDropped(stored.name, held.dropped);
|
|
3038
3142
|
alerts.push(...await this.retryUndelivered(stored.name, state, now, retries));
|
|
3039
3143
|
alerts.push(...await this.dispatch(stored.name, held.alerts, now));
|
|
3144
|
+
alerts.push(...await this.evaluatePending(stored, recent, state, now));
|
|
3040
3145
|
jobs.push(summarize(stored, recent, state, nextExpectedAt, now));
|
|
3041
3146
|
} catch (e) {
|
|
3042
3147
|
this.onError(e, `checking ${stored.name}`);
|
|
@@ -3054,6 +3159,32 @@ ${id}`;
|
|
|
3054
3159
|
}
|
|
3055
3160
|
return { checkedAt: now, jobs, alerts, pruned };
|
|
3056
3161
|
}
|
|
3162
|
+
/**
|
|
3163
|
+
* Runs the job's state still holds as pending (holdRun) whose finish was
|
|
3164
|
+
* written but never evaluated: the process that finished them stopped, or
|
|
3165
|
+
* its state write failed, between the two writes. Each one finished more
|
|
3166
|
+
* than SEND_LEASE_MS ago is evaluated now, as its finish would have been;
|
|
3167
|
+
* one still running is left to the stuck check, and one the store no
|
|
3168
|
+
* longer has (given back, or pruned) is let go. The wait leaves a finish
|
|
3169
|
+
* under way in another process to write its own evaluation.
|
|
3170
|
+
*/
|
|
3171
|
+
async evaluatePending(stored, recent, state, now) {
|
|
3172
|
+
const alerts = [];
|
|
3173
|
+
for (const id of state.pendingRuns ?? []) {
|
|
3174
|
+
try {
|
|
3175
|
+
const run = recent.find((r) => r.id === id) ?? await this.store.getRun(id);
|
|
3176
|
+
if (!run || run.job !== stored.name) {
|
|
3177
|
+
await this.updateState(stored.name, (current) => ({ state: releaseRun(current, id), result: void 0 }));
|
|
3178
|
+
continue;
|
|
3179
|
+
}
|
|
3180
|
+
if (run.status === "running" || typeof run.finishedAt !== "number" || run.finishedAt > now - SEND_LEASE_MS) continue;
|
|
3181
|
+
alerts.push(...await this.finishRun(stored.definition, run, now));
|
|
3182
|
+
} catch (e) {
|
|
3183
|
+
this.onError(e, `evaluating run ${id} of ${stored.name}`);
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
return alerts;
|
|
3187
|
+
}
|
|
3057
3188
|
/** A job's summary and its newest runs, without alerting. A job that cannot be evaluated is reported and shown as failing. */
|
|
3058
3189
|
async snapshot(stored, now, runs) {
|
|
3059
3190
|
let recent = [];
|
|
@@ -3120,7 +3251,7 @@ ${id}`;
|
|
|
3120
3251
|
state.silencedUntil = null;
|
|
3121
3252
|
});
|
|
3122
3253
|
}
|
|
3123
|
-
/** Read, change and write one job's state, in turn with every other update to it. */
|
|
3254
|
+
/** Read, change, and write one job's state, in turn with every other update to it. */
|
|
3124
3255
|
async patchState(name, change) {
|
|
3125
3256
|
await this.ensureReady();
|
|
3126
3257
|
const { state } = await this.updateState(name, (current) => {
|
|
@@ -3132,11 +3263,15 @@ ${id}`;
|
|
|
3132
3263
|
}
|
|
3133
3264
|
/**
|
|
3134
3265
|
* Remove a job and its runs from the store. A job still declared in code
|
|
3135
|
-
* comes back: here on its next run,
|
|
3136
|
-
*
|
|
3266
|
+
* comes back, with no history: here on its next run, declared as it was
|
|
3267
|
+
* written (cw.run() and cw.resumeRun() by name included), and in any other
|
|
3268
|
+
* process that declares it on its next run there, or at that process's
|
|
3269
|
+
* next check or dashboard read.
|
|
3137
3270
|
*/
|
|
3138
3271
|
async forget(name) {
|
|
3139
3272
|
await this.ensureReady();
|
|
3273
|
+
const definition = this.definitions.get(name);
|
|
3274
|
+
if (definition) this.forgotten.set(name, definition);
|
|
3140
3275
|
this.definitions.delete(name);
|
|
3141
3276
|
this.synced.delete(name);
|
|
3142
3277
|
await this.store.deleteJob(name);
|
|
@@ -3189,7 +3324,7 @@ ${id}`;
|
|
|
3189
3324
|
}
|
|
3190
3325
|
/**
|
|
3191
3326
|
* Stop the interval, wait for a check already under way (bounded by its
|
|
3192
|
-
* own channel, triage and retry timeouts; what it throws was reported to
|
|
3327
|
+
* own channel, triage, and retry timeouts; what it throws was reported to
|
|
3193
3328
|
* whoever started it), then close the store, so that check neither writes
|
|
3194
3329
|
* after the store is closed nor loses the alerts it would queue.
|
|
3195
3330
|
*/
|
|
@@ -3221,27 +3356,40 @@ ${id}`;
|
|
|
3221
3356
|
}
|
|
3222
3357
|
/**
|
|
3223
3358
|
* Send the alerts that no channel accepted last time, once each, oldest
|
|
3224
|
-
* first.
|
|
3225
|
-
*
|
|
3226
|
-
*
|
|
3227
|
-
*
|
|
3359
|
+
* first. They are claimed first (claimUndelivered): moved into `sending`
|
|
3360
|
+
* with a lease, in one conditional write, so when several processes check
|
|
3361
|
+
* at once only the one whose write lands sends them. An alert that no
|
|
3362
|
+
* longer describes the job (staleAlert) is dropped instead. Retries across
|
|
3363
|
+
* a check share RETRY_BUDGET_MS of wall-clock time; claimed alerts it has
|
|
3364
|
+
* no time left for go back to the queue for the next check.
|
|
3228
3365
|
*/
|
|
3229
3366
|
async retryUndelivered(name, state, now, budget) {
|
|
3230
|
-
|
|
3231
|
-
|
|
3367
|
+
if ((state.undelivered ?? []).length === 0 || this.deferDelivery) return [];
|
|
3368
|
+
let claimed;
|
|
3369
|
+
try {
|
|
3370
|
+
({ result: claimed } = await this.updateState(name, (previous) => {
|
|
3371
|
+
const claim = claimUndelivered(previous, now, this.now() + SEND_LEASE_MS);
|
|
3372
|
+
return { state: claim.state, result: claim.claimed };
|
|
3373
|
+
}));
|
|
3374
|
+
} catch (e) {
|
|
3375
|
+
this.onError(e, `claiming undelivered alerts for ${name}`);
|
|
3376
|
+
return [];
|
|
3377
|
+
}
|
|
3378
|
+
if (claimed.length === 0) return [];
|
|
3232
3379
|
const delivered = [];
|
|
3233
3380
|
const failed = [];
|
|
3234
|
-
const
|
|
3235
|
-
for (const alert of pending) {
|
|
3236
|
-
if (dropped.includes(alert)) continue;
|
|
3381
|
+
for (const alert of claimed) {
|
|
3237
3382
|
const left = RETRY_BUDGET_MS - budget.spentMs;
|
|
3238
|
-
if (left <= 0)
|
|
3383
|
+
if (left <= 0) {
|
|
3384
|
+
failed.push(alert);
|
|
3385
|
+
continue;
|
|
3386
|
+
}
|
|
3239
3387
|
const started = Date.now();
|
|
3240
3388
|
if (this.triage && alert.type !== "recovered" && alert.triage === void 0) await this.addTriage(alert, Math.min(TRIAGE_TIMEOUT_MS, left));
|
|
3241
3389
|
(await this.deliver(alert) ? delivered : failed).push(alert);
|
|
3242
3390
|
budget.spentMs += Math.max(0, Date.now() - started);
|
|
3243
3391
|
}
|
|
3244
|
-
await this.recordDelivery(name, delivered, failed,
|
|
3392
|
+
await this.recordDelivery(name, delivered, failed, [], now);
|
|
3245
3393
|
return delivered;
|
|
3246
3394
|
}
|
|
3247
3395
|
/**
|