@cronwatch/sdk 0.10.0 → 0.11.1
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 +4 -4
- package/dist/anthropic.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/bugsnag.d.cts +1 -1
- package/dist/bugsnag.d.ts +1 -1
- package/dist/{client-DUoSea7X.d.cts → client-CRMfhylq.d.cts} +63 -26
- package/dist/{client-BcIjVBiZ.d.ts → client-D2Xk2600.d.ts} +63 -26
- package/dist/d1.cjs +29 -11
- package/dist/d1.cjs.map +1 -1
- package/dist/d1.d.cts +1 -1
- package/dist/d1.d.ts +1 -1
- package/dist/d1.js +29 -11
- package/dist/d1.js.map +1 -1
- package/dist/datadog.d.cts +1 -1
- package/dist/datadog.d.ts +1 -1
- package/dist/discord.cjs +10 -4
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +13 -6
- package/dist/discord.d.ts +13 -6
- package/dist/discord.js +7 -1
- package/dist/discord.js.map +1 -1
- package/dist/{email-VrGsVz43.d.ts → email-BWuUiCkw.d.ts} +1 -1
- package/dist/{email-JZs3vxwq.d.cts → email-Dcqe3k8n.d.cts} +1 -1
- package/dist/honeybadger.d.cts +1 -1
- package/dist/honeybadger.d.ts +1 -1
- package/dist/index.cjs +194 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -9
- package/dist/index.d.ts +15 -9
- package/dist/index.js +193 -41
- package/dist/index.js.map +1 -1
- package/dist/mailgun.d.cts +2 -2
- package/dist/mailgun.d.ts +2 -2
- package/dist/newrelic.d.cts +1 -1
- package/dist/newrelic.d.ts +1 -1
- package/dist/pg-cron.cjs +17 -7
- package/dist/pg-cron.cjs.map +1 -1
- package/dist/pg-cron.d.cts +15 -12
- package/dist/pg-cron.d.ts +15 -12
- package/dist/pg-cron.js +14 -4
- package/dist/pg-cron.js.map +1 -1
- package/dist/postgres.cjs +28 -10
- 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 +28 -10
- package/dist/postgres.js.map +1 -1
- package/dist/postmark.d.cts +2 -2
- package/dist/postmark.d.ts +2 -2
- package/dist/resend.d.cts +2 -2
- package/dist/resend.d.ts +2 -2
- package/dist/rollbar.d.cts +1 -1
- package/dist/rollbar.d.ts +1 -1
- package/dist/sendgrid.d.cts +2 -2
- package/dist/sendgrid.d.ts +2 -2
- package/dist/sentry.cjs +4 -1
- package/dist/sentry.cjs.map +1 -1
- package/dist/sentry.d.cts +5 -2
- package/dist/sentry.d.ts +5 -2
- package/dist/sentry.js +4 -1
- package/dist/sentry.js.map +1 -1
- package/dist/ses.d.cts +2 -2
- package/dist/ses.d.ts +2 -2
- package/dist/slack.d.cts +1 -1
- package/dist/slack.d.ts +1 -1
- package/dist/sqlite.cjs +29 -11
- 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 +29 -11
- package/dist/sqlite.js.map +1 -1
- package/dist/twilio.cjs +10 -4
- package/dist/twilio.cjs.map +1 -1
- package/dist/twilio.d.cts +12 -7
- package/dist/twilio.d.ts +12 -7
- package/dist/twilio.js +7 -1
- package/dist/twilio.js.map +1 -1
- package/dist/{types-DyWCxlSi.d.cts → types-DLnlUie8.d.cts} +5 -2
- package/dist/{types-DyWCxlSi.d.ts → types-DLnlUie8.d.ts} +5 -2
- package/dist/webhook.cjs +10 -5
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +18 -7
- package/dist/webhook.d.ts +18 -7
- package/dist/webhook.js +10 -6
- package/dist/webhook.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
export { F as FetchHandler, H as HandlerFn,
|
|
3
|
-
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-
|
|
4
|
-
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule,
|
|
1
|
+
import { C as CronwatchOptions, a as Cronwatch } from './client-CRMfhylq.cjs';
|
|
2
|
+
export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-CRMfhylq.cjs';
|
|
3
|
+
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DLnlUie8.cjs';
|
|
4
|
+
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-DLnlUie8.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Keeps everything in process memory. The default when no store is given,
|
|
@@ -10,6 +10,9 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
|
|
|
10
10
|
*/
|
|
11
11
|
declare function memory(): Store;
|
|
12
12
|
|
|
13
|
+
/** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
|
|
14
|
+
declare const VERSION = "0.11.1";
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* "15m" -> 900000. Accepts a plain number of milliseconds, and compound
|
|
15
18
|
* strings such as "1h30m". Whitespace between parts is fine.
|
|
@@ -28,9 +31,12 @@ interface ParsedSchedule {
|
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
33
|
* "0 2 * * *" (cron, five or six fields), "@hourly", or "every 5m".
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
+
* Text with a colon after its first character, which croner would take for
|
|
35
|
+
* a one-time date, is refused. A cron that names a date no month has
|
|
36
|
+
* ("0 0 30 2 *") is read, and never fires: nothing is ever due, so it is
|
|
37
|
+
* never missed. Parsed once per (schedule, timezone) pair and cached. The
|
|
38
|
+
* croner instance gets no callback, so it schedules nothing; it is only used
|
|
39
|
+
* to compute fire times. Without a timezone the expression is read in the process timezone,
|
|
34
40
|
* like crontab. Vercel and GitHub Actions run their crons in UTC, so pass
|
|
35
41
|
* timezone: "UTC" for those.
|
|
36
42
|
*/
|
|
@@ -47,6 +53,6 @@ declare function composeAlert(draft: AlertDraft, def: StoredJobDefinition, now:
|
|
|
47
53
|
* export const cw = cronwatch({ store: sqlite({ path: "./data/cronwatch.db" }), alerts: [slack({ webhookUrl })] });
|
|
48
54
|
* export const nightly = cw.job("nightly-report", { schedule: "0 2 * * *", grace: "15m" });
|
|
49
55
|
*/
|
|
50
|
-
declare function cronwatch(options?:
|
|
56
|
+
declare function cronwatch(options?: CronwatchOptions): Cronwatch;
|
|
51
57
|
|
|
52
|
-
export { Alert, AlertDraft,
|
|
58
|
+
export { Alert, AlertDraft, Cronwatch, CronwatchOptions, Duration, type ParsedSchedule, Store, StoredJobDefinition, VERSION, composeAlert, cronwatch, formatDuration, memory, nextFire, parseDuration, parseSchedule };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
export { F as FetchHandler, H as HandlerFn,
|
|
3
|
-
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-
|
|
4
|
-
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule,
|
|
1
|
+
import { C as CronwatchOptions, a as Cronwatch } from './client-D2Xk2600.js';
|
|
2
|
+
export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-D2Xk2600.js';
|
|
3
|
+
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DLnlUie8.js';
|
|
4
|
+
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-DLnlUie8.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Keeps everything in process memory. The default when no store is given,
|
|
@@ -10,6 +10,9 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
|
|
|
10
10
|
*/
|
|
11
11
|
declare function memory(): Store;
|
|
12
12
|
|
|
13
|
+
/** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
|
|
14
|
+
declare const VERSION = "0.11.1";
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* "15m" -> 900000. Accepts a plain number of milliseconds, and compound
|
|
15
18
|
* strings such as "1h30m". Whitespace between parts is fine.
|
|
@@ -28,9 +31,12 @@ interface ParsedSchedule {
|
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
33
|
* "0 2 * * *" (cron, five or six fields), "@hourly", or "every 5m".
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
+
* Text with a colon after its first character, which croner would take for
|
|
35
|
+
* a one-time date, is refused. A cron that names a date no month has
|
|
36
|
+
* ("0 0 30 2 *") is read, and never fires: nothing is ever due, so it is
|
|
37
|
+
* never missed. Parsed once per (schedule, timezone) pair and cached. The
|
|
38
|
+
* croner instance gets no callback, so it schedules nothing; it is only used
|
|
39
|
+
* to compute fire times. Without a timezone the expression is read in the process timezone,
|
|
34
40
|
* like crontab. Vercel and GitHub Actions run their crons in UTC, so pass
|
|
35
41
|
* timezone: "UTC" for those.
|
|
36
42
|
*/
|
|
@@ -47,6 +53,6 @@ declare function composeAlert(draft: AlertDraft, def: StoredJobDefinition, now:
|
|
|
47
53
|
* export const cw = cronwatch({ store: sqlite({ path: "./data/cronwatch.db" }), alerts: [slack({ webhookUrl })] });
|
|
48
54
|
* export const nightly = cw.job("nightly-report", { schedule: "0 2 * * *", grace: "15m" });
|
|
49
55
|
*/
|
|
50
|
-
declare function cronwatch(options?:
|
|
56
|
+
declare function cronwatch(options?: CronwatchOptions): Cronwatch;
|
|
51
57
|
|
|
52
|
-
export { Alert, AlertDraft,
|
|
58
|
+
export { Alert, AlertDraft, Cronwatch, CronwatchOptions, Duration, type ParsedSchedule, Store, StoredJobDefinition, VERSION, composeAlert, cronwatch, formatDuration, memory, nextFire, parseDuration, parseSchedule };
|
package/dist/index.js
CHANGED
|
@@ -78,13 +78,46 @@ function beyondDates(at) {
|
|
|
78
78
|
function readEnv(name) {
|
|
79
79
|
return typeof process === "undefined" || !process.env ? void 0 : process.env[name];
|
|
80
80
|
}
|
|
81
|
+
function readSecretEnv(name) {
|
|
82
|
+
const value = readEnv(name);
|
|
83
|
+
return value === void 0 || value.trim() === "" ? void 0 : value;
|
|
84
|
+
}
|
|
85
|
+
function secretOption(value, what) {
|
|
86
|
+
if (value === void 0 || value === null) return value;
|
|
87
|
+
if (typeof value !== "string") {
|
|
88
|
+
throw new TypeError(`${what} must be a string, or null to opt out, not ${Array.isArray(value) ? "an array" : typeof value}`);
|
|
89
|
+
}
|
|
90
|
+
return value.trim() === "" ? void 0 : value;
|
|
91
|
+
}
|
|
92
|
+
function environment() {
|
|
93
|
+
if (typeof process === "undefined" || !process.env) return "";
|
|
94
|
+
for (const value of [process.env.CRONWATCH_ENV, process.env.APP_ENV, process.env.NODE_ENV]) {
|
|
95
|
+
const name = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
96
|
+
if (name === "") continue;
|
|
97
|
+
if (name === "prod") return "production";
|
|
98
|
+
if (name === "dev" || name === "local" || name === "test" || name === "testing") return "development";
|
|
99
|
+
return name;
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
81
103
|
function isDevelopment() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
return environment() === "development";
|
|
105
|
+
}
|
|
106
|
+
function isProduction() {
|
|
107
|
+
return environment() === "production";
|
|
85
108
|
}
|
|
86
109
|
var cache = /* @__PURE__ */ new Map();
|
|
87
110
|
var crons = /* @__PURE__ */ new WeakMap();
|
|
111
|
+
var never = /* @__PURE__ */ new WeakSet();
|
|
112
|
+
var PROBE_FROM_MS = Date.UTC(2e3, 0, 1);
|
|
113
|
+
function neverFires(cron) {
|
|
114
|
+
try {
|
|
115
|
+
return cron.nextRuns(1, new Date(PROBE_FROM_MS)).length === 0;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return error instanceof RangeError && /call stack/i.test(error.message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
var ONE_TIME_DATE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
|
|
88
121
|
var EARLY_SLACK_MS = 6e4;
|
|
89
122
|
function parseSchedule(schedule, timezone) {
|
|
90
123
|
const key = `${timezone ?? ""}|${schedule}`;
|
|
@@ -98,6 +131,10 @@ function parseSchedule(schedule, timezone) {
|
|
|
98
131
|
if (everyMs < 1e3) throw new Error(`schedule "${schedule}" is shorter than one second`);
|
|
99
132
|
parsed = { kind: "interval", source: text, everyMs };
|
|
100
133
|
} else {
|
|
134
|
+
if (text.slice(1).includes(":")) {
|
|
135
|
+
const reason = ONE_TIME_DATE.test(text) ? "CronPattern: a one-time date is not supported" : "Invalid ISO8601 passed to timezone parser.";
|
|
136
|
+
throw new Error(`schedule "${schedule}" is not a cron expression or "every <duration>": ${reason}`);
|
|
137
|
+
}
|
|
101
138
|
let cron;
|
|
102
139
|
try {
|
|
103
140
|
cron = new Cron(text, timezone ? { timezone } : {});
|
|
@@ -106,6 +143,7 @@ function parseSchedule(schedule, timezone) {
|
|
|
106
143
|
}
|
|
107
144
|
parsed = { kind: "cron", source: text, ...timezone ? { timezone } : {} };
|
|
108
145
|
crons.set(parsed, cron);
|
|
146
|
+
if (neverFires(cron)) never.add(parsed);
|
|
109
147
|
}
|
|
110
148
|
cache.set(key, parsed);
|
|
111
149
|
return parsed;
|
|
@@ -132,6 +170,7 @@ function countFrom(from) {
|
|
|
132
170
|
function fireAfter(parsed, from) {
|
|
133
171
|
const cron = crons.get(parsed);
|
|
134
172
|
if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
|
|
173
|
+
if (never.has(parsed)) return null;
|
|
135
174
|
const start = countFrom(from);
|
|
136
175
|
if (start === null) return null;
|
|
137
176
|
let probe = start;
|
|
@@ -147,6 +186,7 @@ function fireAfter(parsed, from) {
|
|
|
147
186
|
function firesBetween(parsed, from, to, limit) {
|
|
148
187
|
const cron = crons.get(parsed);
|
|
149
188
|
if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
|
|
189
|
+
if (never.has(parsed)) return [];
|
|
150
190
|
const out = [];
|
|
151
191
|
const start = countFrom(from);
|
|
152
192
|
if (start === null) return out;
|
|
@@ -281,16 +321,24 @@ function failureCount(state) {
|
|
|
281
321
|
function emptyState(job) {
|
|
282
322
|
return { job, open: {}, consecutiveFailures: 0, silencedUntil: null, lastAlertAt: null, pendingRecovery: [], undelivered: [] };
|
|
283
323
|
}
|
|
324
|
+
function isObject(value) {
|
|
325
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
326
|
+
}
|
|
284
327
|
function normalizeState(state, job) {
|
|
285
|
-
if (!state) return emptyState(job);
|
|
328
|
+
if (!isObject(state)) return emptyState(job);
|
|
286
329
|
const { sending, ...rest } = state;
|
|
330
|
+
const open = isObject(state.open) ? Object.fromEntries(Object.entries(state.open).filter(([, at]) => typeof at === "number")) : {};
|
|
331
|
+
const number = (value) => typeof value === "number" ? value : null;
|
|
332
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
287
333
|
return {
|
|
288
334
|
...emptyState(job),
|
|
289
335
|
...rest,
|
|
290
|
-
open
|
|
336
|
+
open,
|
|
291
337
|
consecutiveFailures: failureCount(state),
|
|
292
|
-
|
|
293
|
-
|
|
338
|
+
silencedUntil: number(state.silencedUntil),
|
|
339
|
+
lastAlertAt: number(state.lastAlertAt),
|
|
340
|
+
pendingRecovery: list(state.pendingRecovery).filter((c) => typeof c === "string"),
|
|
341
|
+
undelivered: list(state.undelivered).filter((alert) => isObject(alert)),
|
|
294
342
|
...Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {}
|
|
295
343
|
};
|
|
296
344
|
}
|
|
@@ -494,8 +542,12 @@ function applySilence(previous, evaluation, now) {
|
|
|
494
542
|
return { state: muteOpens(previous, evaluation.state), alerts: [] };
|
|
495
543
|
}
|
|
496
544
|
function staleAlert(alert, state) {
|
|
497
|
-
if (alert.type === "recovered")
|
|
498
|
-
|
|
545
|
+
if (alert.type === "recovered") {
|
|
546
|
+
const after2 = isObject(alert.details) ? alert.details.after : void 0;
|
|
547
|
+
if (!Array.isArray(after2)) return true;
|
|
548
|
+
return after2.some((condition) => typeof condition !== "string" || state.open[condition] !== void 0);
|
|
549
|
+
}
|
|
550
|
+
return typeof alert.at !== "number" || state.open[alert.type] !== alert.at;
|
|
499
551
|
}
|
|
500
552
|
function jobHealth(def, lastRun, state, now) {
|
|
501
553
|
const open = openConditions(state);
|
|
@@ -808,6 +860,15 @@ function checkExpectation(expect, output) {
|
|
|
808
860
|
}
|
|
809
861
|
return ok ? null : "Output did not pass the expect() check";
|
|
810
862
|
}
|
|
863
|
+
function readStoredJob(stored) {
|
|
864
|
+
const definition = stored.definition;
|
|
865
|
+
if (typeof definition !== "object" || definition === null || Array.isArray(definition)) {
|
|
866
|
+
return { job: { ...stored, definition: { name: stored.name } }, readable: false };
|
|
867
|
+
}
|
|
868
|
+
const read = { ...definition };
|
|
869
|
+
if ("tags" in read && !(Array.isArray(read.tags) && read.tags.every((tag) => typeof tag === "string"))) delete read.tags;
|
|
870
|
+
return { job: { ...stored, definition: read }, readable: true };
|
|
871
|
+
}
|
|
811
872
|
|
|
812
873
|
// src/routes/escape.ts
|
|
813
874
|
function escapeHtml(value) {
|
|
@@ -851,6 +912,21 @@ function manifest(base) {
|
|
|
851
912
|
});
|
|
852
913
|
}
|
|
853
914
|
var APP_JS = `"use strict";
|
|
915
|
+
(function () {
|
|
916
|
+
var root = document.documentElement;
|
|
917
|
+
try {
|
|
918
|
+
var stored = localStorage.getItem("cronwatch-theme");
|
|
919
|
+
if (stored === "light" || stored === "dark") root.setAttribute("data-theme", stored);
|
|
920
|
+
} catch (e) {}
|
|
921
|
+
document.addEventListener("keydown", function (event) {
|
|
922
|
+
if (!(event.metaKey || event.ctrlKey) || !event.shiftKey || event.altKey || event.code !== "KeyD") return;
|
|
923
|
+
event.preventDefault();
|
|
924
|
+
var shown = root.getAttribute("data-theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
925
|
+
var next = shown === "dark" ? "light" : "dark";
|
|
926
|
+
root.setAttribute("data-theme", next);
|
|
927
|
+
try { localStorage.setItem("cronwatch-theme", next); } catch (e) {}
|
|
928
|
+
});
|
|
929
|
+
})();
|
|
854
930
|
(function () {
|
|
855
931
|
var script = document.currentScript;
|
|
856
932
|
if (!script || !("serviceWorker" in navigator)) return;
|
|
@@ -1227,7 +1303,8 @@ function weekRunsLimit(job, now) {
|
|
|
1227
1303
|
var h = escapeHtml;
|
|
1228
1304
|
var CSS = `
|
|
1229
1305
|
:root{color-scheme:light dark;--paper:#f4f4f5;--sheet:#fff;--sunk:#fafafa;--rule:#e4e4e7;--rule-2:#d4d4d8;--tick:#909098;--ink:#000;--body:#18181b;--muted:#71717a;--ok:#15803d;--warn:#a16207;--bad:#b91c1c;--serif:"Newsreader",ui-serif,Georgia,Cambria,"Times New Roman",serif;--mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--who:200px}
|
|
1230
|
-
@media(prefers-color-scheme:dark){:root{--paper:#09090b;--sheet:#111113;--sunk:#18181b;--rule:#27272a;--rule-2:#3f3f46;--tick:#66666f;--ink:#fff;--body:#e4e4e7;--muted:#a1a1aa;--ok:#4ade80;--warn:#fbbf24;--bad:#f87171}}
|
|
1306
|
+
@media(prefers-color-scheme:dark){:root:not([data-theme=light]){--paper:#09090b;--sheet:#111113;--sunk:#18181b;--rule:#27272a;--rule-2:#3f3f46;--tick:#66666f;--ink:#fff;--body:#e4e4e7;--muted:#a1a1aa;--ok:#4ade80;--warn:#fbbf24;--bad:#f87171}}
|
|
1307
|
+
:root[data-theme=light]{color-scheme:light}:root[data-theme=dark]{color-scheme:dark;--paper:#09090b;--sheet:#111113;--sunk:#18181b;--rule:#27272a;--rule-2:#3f3f46;--tick:#66666f;--ink:#fff;--body:#e4e4e7;--muted:#a1a1aa;--ok:#4ade80;--warn:#fbbf24;--bad:#f87171}
|
|
1231
1308
|
*{box-sizing:border-box}html{-webkit-text-size-adjust:100%}
|
|
1232
1309
|
body{margin:0;background:var(--paper);color:var(--ink);font:400 16px/1.55 var(--serif);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
|
|
1233
1310
|
a{color:inherit;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:.16em;text-decoration-color:var(--rule-2)}a:hover{text-decoration-color:currentColor}
|
|
@@ -1579,12 +1656,18 @@ function jobPage(job, runs, now, base, complete = true) {
|
|
|
1579
1656
|
return layout(`${job.name}: CronWatch`, body, base, { refresh: 60 });
|
|
1580
1657
|
}
|
|
1581
1658
|
function messagePage(title, message, base, signIn = false) {
|
|
1582
|
-
const form = signIn ? `<form class="signin" method="
|
|
1659
|
+
const form = signIn ? `<form class="signin" method="post" action="${h(base)}/signin"><label for="token">Token</label><input id="token" name="token" type="password" autocomplete="current-password" autocapitalize="off" spellcheck="false" required><button class="primary" type="submit">Sign in</button></form>` : "";
|
|
1583
1660
|
return layout(title, `<header class="top">${brand(base)}</header><main class="message"><h1>${h(title)}</h1><p>${h(message)}</p>${form}</main>`, base);
|
|
1584
1661
|
}
|
|
1585
1662
|
|
|
1663
|
+
// src/version.ts
|
|
1664
|
+
var VERSION = "0.11.1";
|
|
1665
|
+
var API_VERSION = 1;
|
|
1666
|
+
|
|
1586
1667
|
// src/routes/index.ts
|
|
1587
1668
|
var COOKIE = "cronwatch_token";
|
|
1669
|
+
var LIBRARY = "@cronwatch/sdk";
|
|
1670
|
+
var LANGUAGE = "typescript";
|
|
1588
1671
|
async function cookieValue(token) {
|
|
1589
1672
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`cronwatch-cookie:${token}`));
|
|
1590
1673
|
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -1668,6 +1751,20 @@ function forwardedOrigin(request, url) {
|
|
|
1668
1751
|
return url.origin;
|
|
1669
1752
|
}
|
|
1670
1753
|
}
|
|
1754
|
+
function bearerToken(header) {
|
|
1755
|
+
const match = header === null ? null : /^Bearer\s+([\s\S]*)$/i.exec(header);
|
|
1756
|
+
return match ? match[1] : null;
|
|
1757
|
+
}
|
|
1758
|
+
function signInReturn(referer, publicOrigin, base) {
|
|
1759
|
+
if (referer === null || !referer.startsWith(publicOrigin + "/")) return `${base}/`;
|
|
1760
|
+
let url;
|
|
1761
|
+
try {
|
|
1762
|
+
url = new URL(referer);
|
|
1763
|
+
} catch {
|
|
1764
|
+
return `${base}/`;
|
|
1765
|
+
}
|
|
1766
|
+
return url.searchParams.has("token") ? `${base}/` : referer;
|
|
1767
|
+
}
|
|
1671
1768
|
function stripBase(pathname, base) {
|
|
1672
1769
|
let path = pathname.startsWith(base) ? pathname.slice(base.length) : pathname;
|
|
1673
1770
|
if (path === "") path = "/";
|
|
@@ -1710,8 +1807,12 @@ function runsLimit(value) {
|
|
|
1710
1807
|
return Number.isFinite(n) ? Math.min(MAX_RUNS, Math.max(1, n)) : DEFAULT_RUNS;
|
|
1711
1808
|
}
|
|
1712
1809
|
function createRoutes(cw, options = {}) {
|
|
1810
|
+
return cw.routes(options);
|
|
1811
|
+
}
|
|
1812
|
+
function buildRoutes(cw, options = {}) {
|
|
1713
1813
|
const optedOut = options.token === null;
|
|
1714
|
-
const
|
|
1814
|
+
const given = secretOption(options.token, "routes: token");
|
|
1815
|
+
const configured = optedOut ? null : given || readSecretEnv("CRONWATCH_TOKEN") || null;
|
|
1715
1816
|
const base = (options.basePath ?? "/cronwatch").replace(/\/+$/, "");
|
|
1716
1817
|
const developing = isDevelopment();
|
|
1717
1818
|
const fixedOrigin = configuredOrigin(options.origin);
|
|
@@ -1743,7 +1844,17 @@ function createRoutes(cw, options = {}) {
|
|
|
1743
1844
|
if (method !== "GET" && method !== "HEAD" && crossSite(request, publicOrigin)) {
|
|
1744
1845
|
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);
|
|
1745
1846
|
}
|
|
1746
|
-
const bearer = request.headers.get("authorization")
|
|
1847
|
+
const bearer = bearerToken(request.headers.get("authorization"));
|
|
1848
|
+
const signInCookie = async () => {
|
|
1849
|
+
const secure = publicOrigin.startsWith("https:") ? "; Secure" : "";
|
|
1850
|
+
return `${COOKIE}=${await expectedCookie(token)}; Path=${base || "/"}; HttpOnly; SameSite=Lax; Max-Age=${60 * 60 * 24 * 30}${secure}`;
|
|
1851
|
+
};
|
|
1852
|
+
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);
|
|
1853
|
+
if (token && method === "POST" && path === "/signin") {
|
|
1854
|
+
const sent = (await readBody(request)).token;
|
|
1855
|
+
if (sent === void 0 || !constantTimeEqual(sent, token)) return signInPage();
|
|
1856
|
+
return redirect(signInReturn(request.headers.get("referer"), publicOrigin, base), { "set-cookie": await signInCookie() });
|
|
1857
|
+
}
|
|
1747
1858
|
if (token) {
|
|
1748
1859
|
const query = wantsHtml && method === "GET" ? url.searchParams.get("token") : null;
|
|
1749
1860
|
const sent = readCookie(request, COOKIE);
|
|
@@ -1751,17 +1862,12 @@ function createRoutes(cw, options = {}) {
|
|
|
1751
1862
|
const cronSecretOk = isCheck && bearer !== null && cw.cronSecret !== null && constantTimeEqual(bearer, cw.cronSecret);
|
|
1752
1863
|
const tokenOk = bearer !== null ? constantTimeEqual(bearer, token) : query !== null ? constantTimeEqual(query, token) : sent !== null && constantTimeEqual(sent, await expectedCookie(token));
|
|
1753
1864
|
if (!cronSecretOk && !tokenOk) {
|
|
1754
|
-
if (
|
|
1755
|
-
|
|
1756
|
-
}
|
|
1757
|
-
return wantsHtml ? html(messagePage("Sign in", `Open this page with ?token=<your CRONWATCH_TOKEN> once and it will stay signed in.`, base, true), 401) : api({ ok: false, error: "Unauthorized" }, 401);
|
|
1865
|
+
if (wantsHtml) return signInPage();
|
|
1866
|
+
return generated ? api({ ok: false, error: "Unauthorized: CRONWATCH_TOKEN is not set, so this development server made a token; it is in the server log" }, 401) : api({ ok: false, error: "Unauthorized" }, 401);
|
|
1758
1867
|
}
|
|
1759
1868
|
if (query !== null) {
|
|
1760
1869
|
url.searchParams.delete("token");
|
|
1761
|
-
|
|
1762
|
-
return redirect(url.pathname + (url.search || ""), {
|
|
1763
|
-
"set-cookie": `${COOKIE}=${await expectedCookie(token)}; Path=${base || "/"}; HttpOnly; SameSite=Lax; Max-Age=${60 * 60 * 24 * 30}${secure}`
|
|
1764
|
-
});
|
|
1870
|
+
return redirect(url.pathname + (url.search || ""), { "set-cookie": await signInCookie() });
|
|
1765
1871
|
}
|
|
1766
1872
|
}
|
|
1767
1873
|
const redirectBack = () => {
|
|
@@ -1812,6 +1918,9 @@ function createRoutes(cw, options = {}) {
|
|
|
1812
1918
|
}
|
|
1813
1919
|
if (parts[0] === "api") {
|
|
1814
1920
|
const rest = parts.slice(1);
|
|
1921
|
+
if (method === "GET" && rest.length === 0) {
|
|
1922
|
+
return api({ ok: true, library: LIBRARY, language: LANGUAGE, version: VERSION, api: API_VERSION });
|
|
1923
|
+
}
|
|
1815
1924
|
if (method === "GET" && rest[0] === "jobs" && rest.length === 1) {
|
|
1816
1925
|
return api({ ok: true, jobs: await cw.jobs() });
|
|
1817
1926
|
}
|
|
@@ -1839,9 +1948,13 @@ function createRoutes(cw, options = {}) {
|
|
|
1839
1948
|
} catch (e) {
|
|
1840
1949
|
return api({ ok: false, error: e.message }, 400);
|
|
1841
1950
|
}
|
|
1842
|
-
|
|
1951
|
+
await cw.silence(name, duration);
|
|
1952
|
+
return api({ ok: true, job: await cw.jobSummary(name) });
|
|
1953
|
+
}
|
|
1954
|
+
if (rest[2] === "unsilence") {
|
|
1955
|
+
await cw.unsilence(name);
|
|
1956
|
+
return api({ ok: true, job: await cw.jobSummary(name) });
|
|
1843
1957
|
}
|
|
1844
|
-
if (rest[2] === "unsilence") return api({ ok: true, state: await cw.unsilence(name) });
|
|
1845
1958
|
}
|
|
1846
1959
|
if (rest[0] === "check" && rest.length === 1) {
|
|
1847
1960
|
if (method === "GET" && bearer === null) {
|
|
@@ -2049,7 +2162,7 @@ function validateDefinition(def) {
|
|
|
2049
2162
|
function sameState(a, b) {
|
|
2050
2163
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
2051
2164
|
}
|
|
2052
|
-
var
|
|
2165
|
+
var Cronwatch = class {
|
|
2053
2166
|
store;
|
|
2054
2167
|
alerts;
|
|
2055
2168
|
triage;
|
|
@@ -2062,7 +2175,7 @@ var CronWatch = class {
|
|
|
2062
2175
|
/** cronSecret was passed as null: handlers may run without a secret. */
|
|
2063
2176
|
secretOptOut;
|
|
2064
2177
|
redact;
|
|
2065
|
-
/** "check": queue alerts for another process's check instead of sending them. See
|
|
2178
|
+
/** "check": queue alerts for another process's check instead of sending them. See CronwatchOptions.deliver. */
|
|
2066
2179
|
deferDelivery;
|
|
2067
2180
|
defaults;
|
|
2068
2181
|
definitions = /* @__PURE__ */ new Map();
|
|
@@ -2081,12 +2194,15 @@ var CronWatch = class {
|
|
|
2081
2194
|
usingDefaultStore = false;
|
|
2082
2195
|
warnedNoSecret = false;
|
|
2083
2196
|
warnedDeferredStart = false;
|
|
2197
|
+
/** Stored jobs read with a definition that was not a JSON object (see readJob). */
|
|
2198
|
+
unreadable = /* @__PURE__ */ new WeakSet();
|
|
2084
2199
|
constructor(options = {}) {
|
|
2085
2200
|
this.store = options.store ?? (this.usingDefaultStore = true, memory());
|
|
2086
2201
|
this.alerts = options.alerts ?? [consoleChannel()];
|
|
2087
2202
|
this.triage = options.triage;
|
|
2088
2203
|
this.sources = options.sources ?? [];
|
|
2089
|
-
const
|
|
2204
|
+
const given = secretOption(options.cronSecret, "cronSecret");
|
|
2205
|
+
const secret = options.cronSecret === void 0 ? readSecretEnv("CRON_SECRET") : given;
|
|
2090
2206
|
this.cronSecret = secret ? secret : null;
|
|
2091
2207
|
this.secretOptOut = options.cronSecret === null;
|
|
2092
2208
|
this.retentionMs = parseDuration(options.retention ?? "30d", "retention");
|
|
@@ -2147,7 +2263,7 @@ var CronWatch = class {
|
|
|
2147
2263
|
return outcome.result;
|
|
2148
2264
|
},
|
|
2149
2265
|
handler(fn, options) {
|
|
2150
|
-
const own = options?.secret;
|
|
2266
|
+
const own = secretOption(options?.secret, "handler: secret");
|
|
2151
2267
|
const secret = own === null ? null : own ? own : self.cronSecret;
|
|
2152
2268
|
const optedOut = own === null || !own && self.secretOptOut;
|
|
2153
2269
|
return async (request) => {
|
|
@@ -2199,7 +2315,7 @@ var CronWatch = class {
|
|
|
2199
2315
|
if (!this.ready) {
|
|
2200
2316
|
this.ready = (async () => {
|
|
2201
2317
|
if (this.store.init) await this.store.init();
|
|
2202
|
-
if (this.usingDefaultStore &&
|
|
2318
|
+
if (this.usingDefaultStore && isProduction()) {
|
|
2203
2319
|
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.");
|
|
2204
2320
|
}
|
|
2205
2321
|
})().catch((error) => {
|
|
@@ -2249,13 +2365,23 @@ var CronWatch = class {
|
|
|
2249
2365
|
const jobs = await this.store.listJobs();
|
|
2250
2366
|
const listed = new Set(jobs.map((job) => job.name));
|
|
2251
2367
|
const missing = [...this.definitions.values()].filter((definition) => !listed.has(definition.name));
|
|
2252
|
-
if (missing.length === 0) return jobs;
|
|
2368
|
+
if (missing.length === 0) return jobs.map((job) => this.readJob(job));
|
|
2253
2369
|
for (const definition of missing) {
|
|
2254
2370
|
if (this.definitions.get(definition.name) !== definition) continue;
|
|
2255
2371
|
this.synced.delete(definition.name);
|
|
2256
2372
|
await this.sync(definition);
|
|
2257
2373
|
}
|
|
2258
|
-
return this.store.listJobs();
|
|
2374
|
+
return (await this.store.listJobs()).map((job) => this.readJob(job));
|
|
2375
|
+
}
|
|
2376
|
+
/** A stored job read leniently (readStoredJob); one whose definition is unreadable is remembered, so that evaluating it throws. */
|
|
2377
|
+
readJob(stored) {
|
|
2378
|
+
const { job, readable } = readStoredJob(stored);
|
|
2379
|
+
if (!readable) this.unreadable.add(job);
|
|
2380
|
+
return job;
|
|
2381
|
+
}
|
|
2382
|
+
/** Throws for a job whose stored definition was not a JSON object: reported, and shown as failing, while the others carry on. */
|
|
2383
|
+
evaluable(stored) {
|
|
2384
|
+
if (this.unreadable.has(stored)) throw new Error(`job "${stored.name}": its stored definition is not a JSON object`);
|
|
2259
2385
|
}
|
|
2260
2386
|
/**
|
|
2261
2387
|
* Runs `fn` after every earlier state update for the same job has settled,
|
|
@@ -2674,6 +2800,10 @@ ${id}`;
|
|
|
2674
2800
|
async recordRun(input, options = {}) {
|
|
2675
2801
|
const declared = this.definitions.get(input.job);
|
|
2676
2802
|
if (!declared) throw new Error(`recordRun: job "${input.job}" is not declared; call job() first`);
|
|
2803
|
+
if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > MAX_RUN_ID) {
|
|
2804
|
+
const got = typeof input.id === "string" ? `${input.id.length} characters` : typeof input.id;
|
|
2805
|
+
throw new Error(`recordRun: run ids must be 1 to ${MAX_RUN_ID} characters (got ${got}; job "${input.job}")`);
|
|
2806
|
+
}
|
|
2677
2807
|
if (input.id.includes("\0")) throw new Error(`recordRun: run ids cannot contain a NUL character (job "${input.job}")`);
|
|
2678
2808
|
for (const [metric, value] of Object.entries(input.metrics ?? {})) {
|
|
2679
2809
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -2802,7 +2932,10 @@ ${id}`;
|
|
|
2802
2932
|
for (const listed of await this.store.runningRuns()) {
|
|
2803
2933
|
try {
|
|
2804
2934
|
const declared = this.definitions.get(listed.job);
|
|
2805
|
-
const
|
|
2935
|
+
const found = declared ? null : await this.store.getJob(listed.job);
|
|
2936
|
+
const stored = found ? this.readJob(found) : null;
|
|
2937
|
+
if (stored) this.evaluable(stored);
|
|
2938
|
+
const definition = declared ? toStored(declared) : stored?.definition;
|
|
2806
2939
|
if (!definition || !isStuck(definition, listed, now)) continue;
|
|
2807
2940
|
const run = await this.store.getRun(listed.id);
|
|
2808
2941
|
if (!run || run.status !== "running" || run.job !== listed.job) continue;
|
|
@@ -2821,6 +2954,7 @@ ${id}`;
|
|
|
2821
2954
|
const retries = { spentMs: 0 };
|
|
2822
2955
|
for (const stored of await this.storedJobs()) {
|
|
2823
2956
|
try {
|
|
2957
|
+
this.evaluable(stored);
|
|
2824
2958
|
const recent = await this.store.listRuns(stored.name, BASELINE_WINDOW);
|
|
2825
2959
|
let nextExpectedAt = null;
|
|
2826
2960
|
const { state, result: held } = await this.updateState(stored.name, (previous) => {
|
|
@@ -2856,6 +2990,7 @@ ${id}`;
|
|
|
2856
2990
|
let recent = [];
|
|
2857
2991
|
try {
|
|
2858
2992
|
recent = await this.store.listRuns(stored.name, Math.max(runs, BASELINE_WINDOW));
|
|
2993
|
+
this.evaluable(stored);
|
|
2859
2994
|
const state = await this.readState(stored.name);
|
|
2860
2995
|
const { nextExpectedAt } = onCheck(stored.definition, stored, recent[0] ?? null, state, now);
|
|
2861
2996
|
return { job: summarize(stored, recent, state, nextExpectedAt, now), runs: recent.slice(0, runs) };
|
|
@@ -2890,7 +3025,7 @@ ${id}`;
|
|
|
2890
3025
|
if (definition) await this.sync(definition, true);
|
|
2891
3026
|
const stored = await this.store.getJob(name);
|
|
2892
3027
|
if (!stored) return null;
|
|
2893
|
-
return (await this.snapshot(stored, this.now(), 0)).job;
|
|
3028
|
+
return (await this.snapshot(this.readJob(stored), this.now(), 0)).job;
|
|
2894
3029
|
}
|
|
2895
3030
|
/** A job's runs, newest first. `limit` is a whole number from 1 to 500. */
|
|
2896
3031
|
async runs(name, limit = 50) {
|
|
@@ -2937,21 +3072,27 @@ ${id}`;
|
|
|
2937
3072
|
this.synced.delete(name);
|
|
2938
3073
|
await this.store.deleteJob(name);
|
|
2939
3074
|
}
|
|
2940
|
-
/**
|
|
3075
|
+
/**
|
|
3076
|
+
* The dashboard and JSON API as fetch-style handlers: the one way to mount
|
|
3077
|
+
* them. A Next.js app exports them from app/cronwatch/[[...path]]/route.ts:
|
|
3078
|
+
*
|
|
3079
|
+
* export const { GET, POST, DELETE } = cw.routes();
|
|
3080
|
+
*/
|
|
2941
3081
|
routes(options) {
|
|
2942
|
-
return
|
|
3082
|
+
return buildRoutes(this, options);
|
|
2943
3083
|
}
|
|
2944
3084
|
/**
|
|
2945
3085
|
* Check on an interval, for long-running servers. Default every minute.
|
|
2946
3086
|
* Not for serverless functions or Cloudflare Workers, where nothing runs
|
|
2947
|
-
* between requests: call check() from a cron there instead.
|
|
3087
|
+
* between requests: call check() from a cron there instead. (A job's
|
|
3088
|
+
* start() opens a run; this starts the checks.)
|
|
2948
3089
|
*/
|
|
2949
|
-
|
|
3090
|
+
startChecking(every = "1m") {
|
|
2950
3091
|
if (this.timer) return;
|
|
2951
3092
|
const ms = Math.min(TIMER_MAX_MS, Math.max(5e3, parseDuration(every, "check interval")));
|
|
2952
3093
|
if (this.deferDelivery && !this.warnedDeferredStart) {
|
|
2953
3094
|
this.warnedDeferredStart = true;
|
|
2954
|
-
console.warn('[cronwatch]
|
|
3095
|
+
console.warn('[cronwatch] startChecking() was called with deliver: "check", so these checks send no alerts. Another process must run checks with deliver: "now" (the default) to send them.');
|
|
2955
3096
|
}
|
|
2956
3097
|
const tick = () => this.check().catch((e) => this.onError(e, "check"));
|
|
2957
3098
|
this.timer = setInterval(tick, ms);
|
|
@@ -2962,6 +3103,15 @@ ${id}`;
|
|
|
2962
3103
|
}, 1e3);
|
|
2963
3104
|
if (typeof this.firstTick === "object" && "unref" in this.firstTick) this.firstTick.unref();
|
|
2964
3105
|
}
|
|
3106
|
+
/**
|
|
3107
|
+
* startChecking(), under its former name.
|
|
3108
|
+
* @deprecated Renamed `startChecking`, since a job's `start()` opens a run.
|
|
3109
|
+
* This name still works through 1.x and goes in 2.0.
|
|
3110
|
+
*/
|
|
3111
|
+
start(every = "1m") {
|
|
3112
|
+
this.startChecking(every);
|
|
3113
|
+
}
|
|
3114
|
+
/** Stop the checks startChecking() began. */
|
|
2965
3115
|
stop() {
|
|
2966
3116
|
if (this.timer) clearInterval(this.timer);
|
|
2967
3117
|
if (this.firstTick) clearTimeout(this.firstTick);
|
|
@@ -3074,9 +3224,10 @@ ${id}`;
|
|
|
3074
3224
|
}
|
|
3075
3225
|
};
|
|
3076
3226
|
var RESERVED_RUN_ID_PREFIX = "pgcron:";
|
|
3227
|
+
var MAX_RUN_ID = 200;
|
|
3077
3228
|
function checkRunId(job, id, method) {
|
|
3078
|
-
if (typeof id !== "string" || id.length === 0 || id.length >
|
|
3079
|
-
throw new Error(`job "${job}": ${method}() needs a run id of 1 to
|
|
3229
|
+
if (typeof id !== "string" || id.length === 0 || id.length > MAX_RUN_ID) {
|
|
3230
|
+
throw new Error(`job "${job}": ${method}() needs a run id of 1 to ${MAX_RUN_ID} characters (got ${typeof id === "string" ? `${id.length} characters` : typeof id})`);
|
|
3080
3231
|
}
|
|
3081
3232
|
if (id.includes("\0")) throw new Error(`job "${job}": ${method}() cannot take a run id containing a NUL character`);
|
|
3082
3233
|
if (id.startsWith(RESERVED_RUN_ID_PREFIX)) {
|
|
@@ -3137,12 +3288,13 @@ function custom(name, send) {
|
|
|
3137
3288
|
await send(alert);
|
|
3138
3289
|
} };
|
|
3139
3290
|
}
|
|
3291
|
+
var CronWatch = Cronwatch;
|
|
3140
3292
|
|
|
3141
3293
|
// src/index.ts
|
|
3142
3294
|
function cronwatch(options = {}) {
|
|
3143
|
-
return new
|
|
3295
|
+
return new Cronwatch(options);
|
|
3144
3296
|
}
|
|
3145
3297
|
|
|
3146
|
-
export { CronWatch, composeAlert, consoleChannel, createRoutes, cronwatch, custom, formatDuration, memory, nextFire, parseDuration, parseSchedule };
|
|
3298
|
+
export { CronWatch, Cronwatch, VERSION, composeAlert, consoleChannel, createRoutes, cronwatch, custom, formatDuration, memory, nextFire, parseDuration, parseSchedule };
|
|
3147
3299
|
//# sourceMappingURL=index.js.map
|
|
3148
3300
|
//# sourceMappingURL=index.js.map
|