@cronwatch/sdk 0.14.2 → 0.14.3
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/dist/index.cjs +81 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +81 -30
- package/dist/index.js.map +1 -1
- package/dist/pg-cron.cjs.map +1 -1
- package/dist/pg-cron.js.map +1 -1
- package/dist/slack.cjs +7 -2
- package/dist/slack.cjs.map +1 -1
- package/dist/slack.js +7 -2
- package/dist/slack.js.map +1 -1
- package/dist/webhook.cjs +1 -8
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.js +1 -8
- package/dist/webhook.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
|
|
|
11
11
|
declare function memory(): Store & Required<Pick<Store, "lastRun">>;
|
|
12
12
|
|
|
13
13
|
/** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
|
|
14
|
-
declare const VERSION = "0.14.
|
|
14
|
+
declare const VERSION = "0.14.3";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* "15m" -> 900000. Accepts a plain number of milliseconds, and compound
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
|
|
|
11
11
|
declare function memory(): Store & Required<Pick<Store, "lastRun">>;
|
|
12
12
|
|
|
13
13
|
/** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
|
|
14
|
-
declare const VERSION = "0.14.
|
|
14
|
+
declare const VERSION = "0.14.3";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* "15m" -> 900000. Accepts a plain number of milliseconds, and compound
|
package/dist/index.js
CHANGED
|
@@ -722,6 +722,9 @@ function onCheck(def, stored, lastRun, state, now) {
|
|
|
722
722
|
function isStuck(def, run, now) {
|
|
723
723
|
return run.status === "running" && run.finishedAt === null && now - run.startedAt > timeoutMs(def);
|
|
724
724
|
}
|
|
725
|
+
function isStranded(run, now) {
|
|
726
|
+
return run.status === "running" && run.finishedAt !== null && now - run.finishedAt > SEND_LEASE_MS;
|
|
727
|
+
}
|
|
725
728
|
function muteOpens(previous, next) {
|
|
726
729
|
const muted = cloneState(next);
|
|
727
730
|
for (const condition of openConditions(muted)) {
|
|
@@ -897,6 +900,10 @@ function constantTimeEqual(sent, secret) {
|
|
|
897
900
|
for (let i = 0; i < sent.length; i++) diff |= sent.charCodeAt(i) ^ secret.charCodeAt(i % secret.length);
|
|
898
901
|
return diff === 0;
|
|
899
902
|
}
|
|
903
|
+
function bearerToken(header) {
|
|
904
|
+
const match = header === null ? null : /^Bearer\s+([\s\S]*)$/i.exec(header);
|
|
905
|
+
return match ? match[1] : null;
|
|
906
|
+
}
|
|
900
907
|
|
|
901
908
|
// src/output.ts
|
|
902
909
|
var OUTPUT_CAP = 16 * 1024;
|
|
@@ -990,15 +997,45 @@ function stringify(part) {
|
|
|
990
997
|
return String(part);
|
|
991
998
|
}
|
|
992
999
|
}
|
|
1000
|
+
var LineWindow = class {
|
|
1001
|
+
items = [];
|
|
1002
|
+
start = 0;
|
|
1003
|
+
size = 0;
|
|
1004
|
+
get count() {
|
|
1005
|
+
return this.items.length - this.start;
|
|
1006
|
+
}
|
|
1007
|
+
push(line) {
|
|
1008
|
+
this.items.push(line);
|
|
1009
|
+
this.size += line.length + 1;
|
|
1010
|
+
}
|
|
1011
|
+
/** Drops the oldest lines while over `max`, always keeping the newest. */
|
|
1012
|
+
keepAtMost(max) {
|
|
1013
|
+
while (this.size > max && this.count > 1) this.drop();
|
|
1014
|
+
}
|
|
1015
|
+
/** Drops the oldest lines only while what is left still holds `min`. */
|
|
1016
|
+
keepAtLeast(min) {
|
|
1017
|
+
while (this.count > 1 && this.size - (this.items[this.start].length + 1) > min) this.drop();
|
|
1018
|
+
}
|
|
1019
|
+
join() {
|
|
1020
|
+
return this.items.slice(this.start).join("\n");
|
|
1021
|
+
}
|
|
1022
|
+
drop() {
|
|
1023
|
+
this.size -= this.items[this.start].length + 1;
|
|
1024
|
+
this.start += 1;
|
|
1025
|
+
if (this.start >= 1024 && this.start * 2 >= this.items.length) {
|
|
1026
|
+
this.items = this.items.slice(this.start);
|
|
1027
|
+
this.start = 0;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
993
1031
|
function createRecorder(run) {
|
|
994
1032
|
const window = OUTPUT_CAP + REDACT_EDGE + 2;
|
|
995
1033
|
const copy = (text) => JSON.parse(JSON.stringify(text));
|
|
996
|
-
const lines =
|
|
997
|
-
let size = 0;
|
|
1034
|
+
const lines = new LineWindow();
|
|
998
1035
|
const head = [];
|
|
999
1036
|
let headSize = 0;
|
|
1000
1037
|
let whole = "";
|
|
1001
|
-
|
|
1038
|
+
const raw = new LineWindow();
|
|
1002
1039
|
let logged = false;
|
|
1003
1040
|
const metrics = {};
|
|
1004
1041
|
const controller = new AbortController();
|
|
@@ -1012,7 +1049,8 @@ function createRecorder(run) {
|
|
|
1012
1049
|
const separator = logged ? "\n" : "";
|
|
1013
1050
|
logged = true;
|
|
1014
1051
|
if (whole !== null) whole = whole.length + separator.length + line.length <= 2 * OUTPUT_CAP ? whole + separator + line : null;
|
|
1015
|
-
|
|
1052
|
+
raw.push(copy(line.slice(-OUTPUT_CAP)));
|
|
1053
|
+
raw.keepAtLeast(OUTPUT_CAP);
|
|
1016
1054
|
if (headSize < OUTPUT_CAP) {
|
|
1017
1055
|
head.push(copy(line.slice(0, OUTPUT_CAP - headSize)));
|
|
1018
1056
|
headSize += line.length + 1;
|
|
@@ -1022,10 +1060,7 @@ function createRecorder(run) {
|
|
|
1022
1060
|
line = copy(line.slice(-68609));
|
|
1023
1061
|
}
|
|
1024
1062
|
lines.push(line);
|
|
1025
|
-
|
|
1026
|
-
while (size > window && lines.length > 1) {
|
|
1027
|
-
size -= lines.shift().length + 1;
|
|
1028
|
-
}
|
|
1063
|
+
lines.keepAtMost(window);
|
|
1029
1064
|
},
|
|
1030
1065
|
metric(name, value) {
|
|
1031
1066
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -1039,10 +1074,10 @@ function createRecorder(run) {
|
|
|
1039
1074
|
};
|
|
1040
1075
|
return {
|
|
1041
1076
|
context,
|
|
1042
|
-
output: () => lines.
|
|
1077
|
+
output: () => lines.count === 0 ? null : lines.join(),
|
|
1043
1078
|
expectText: () => {
|
|
1044
1079
|
if (!logged) return null;
|
|
1045
|
-
return whole ?? head.join("\n").slice(0, OUTPUT_CAP) + "\n" +
|
|
1080
|
+
return whole ?? head.join("\n").slice(0, OUTPUT_CAP) + "\n" + raw.join().slice(-OUTPUT_CAP);
|
|
1046
1081
|
},
|
|
1047
1082
|
metrics: () => ({ ...metrics }),
|
|
1048
1083
|
abort: () => controller.abort()
|
|
@@ -1216,7 +1251,7 @@ function staticAsset(path, base) {
|
|
|
1216
1251
|
}
|
|
1217
1252
|
|
|
1218
1253
|
// src/version.ts
|
|
1219
|
-
var VERSION = "0.14.
|
|
1254
|
+
var VERSION = "0.14.3";
|
|
1220
1255
|
var API_VERSION = 1;
|
|
1221
1256
|
|
|
1222
1257
|
// src/routes/timeline.ts
|
|
@@ -1983,10 +2018,6 @@ function forwardedOrigin(request, url) {
|
|
|
1983
2018
|
return url.origin;
|
|
1984
2019
|
}
|
|
1985
2020
|
}
|
|
1986
|
-
function bearerToken(header) {
|
|
1987
|
-
const match = header === null ? null : /^Bearer\s+([\s\S]*)$/i.exec(header);
|
|
1988
|
-
return match ? match[1] : null;
|
|
1989
|
-
}
|
|
1990
2021
|
function signInReturn(referer, publicOrigin, base) {
|
|
1991
2022
|
if (referer === null || !referer.startsWith(publicOrigin + "/")) return `${base}/`;
|
|
1992
2023
|
let url;
|
|
@@ -2155,13 +2186,14 @@ function buildRoutes(cw, options = {}) {
|
|
|
2155
2186
|
return wantsHtml ? html(messagePage("Bad request", "The path is not valid.", base), 400) : api({ ok: false, error: "Bad path" }, 400);
|
|
2156
2187
|
}
|
|
2157
2188
|
const parts = decoded;
|
|
2158
|
-
|
|
2189
|
+
const reads = method === "GET" || method === "HEAD";
|
|
2190
|
+
if (reads && path === "/") {
|
|
2159
2191
|
const entries = await cw.jobsWithRuns(BOARD_PAGE_RUNS);
|
|
2160
2192
|
const now = cw.now();
|
|
2161
2193
|
const runsByJob = new Map(entries.map((entry) => [entry.job.name, entry.runs]));
|
|
2162
2194
|
return html(dashboardPage(entries.map((entry) => entry.job), runsByJob, now, base, null, await boardLanes(cw, entries, now)));
|
|
2163
2195
|
}
|
|
2164
|
-
if (
|
|
2196
|
+
if (reads && parts[0] === "jobs" && parts.length === 2) {
|
|
2165
2197
|
const job = await cw.jobSummary(parts[1]);
|
|
2166
2198
|
if (!job) return html(messagePage("No such job", `${parts[1]} is not in the store.`, base), 404);
|
|
2167
2199
|
const now = cw.now();
|
|
@@ -2197,15 +2229,15 @@ function buildRoutes(cw, options = {}) {
|
|
|
2197
2229
|
}
|
|
2198
2230
|
if (parts[0] === "api") {
|
|
2199
2231
|
const rest = parts.slice(1);
|
|
2200
|
-
if (
|
|
2232
|
+
if (reads && rest.length === 0) {
|
|
2201
2233
|
return api({ ok: true, library: LIBRARY, language: LANGUAGE, version: VERSION, api: API_VERSION });
|
|
2202
2234
|
}
|
|
2203
|
-
if (
|
|
2235
|
+
if (reads && rest[0] === "jobs" && rest.length === 1) {
|
|
2204
2236
|
return api({ ok: true, jobs: await cw.jobs() });
|
|
2205
2237
|
}
|
|
2206
2238
|
if (rest[0] === "jobs" && rest.length === 2) {
|
|
2207
2239
|
const name = rest[1];
|
|
2208
|
-
if (
|
|
2240
|
+
if (reads) {
|
|
2209
2241
|
const job = await cw.jobSummary(name);
|
|
2210
2242
|
if (!job) return api({ ok: false, error: "No such job" }, 404);
|
|
2211
2243
|
return api({ ok: true, job, runs: await cw.runs(name, runsLimit(url.searchParams.get("runs"))) });
|
|
@@ -2246,7 +2278,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
2246
2278
|
return api({ ok: true, ...result });
|
|
2247
2279
|
}
|
|
2248
2280
|
}
|
|
2249
|
-
if (
|
|
2281
|
+
if (reads && rest[0] === "runs" && rest.length === 2) {
|
|
2250
2282
|
const run = await cw.getRun(rest[1]);
|
|
2251
2283
|
return run ? api({ ok: true, run }) : api({ ok: false, error: "No such run" }, 404);
|
|
2252
2284
|
}
|
|
@@ -2261,7 +2293,8 @@ function buildRoutes(cw, options = {}) {
|
|
|
2261
2293
|
const path = stripBase(url.pathname, base);
|
|
2262
2294
|
if (path === null) return html(messagePage("Not found", url.pathname, base), 404);
|
|
2263
2295
|
wantsHtml = !path.startsWith("/api");
|
|
2264
|
-
|
|
2296
|
+
const response = await serve(request, url, path, wantsHtml);
|
|
2297
|
+
return request.method.toUpperCase() === "HEAD" ? new Response(null, { status: response.status, headers: response.headers }) : response;
|
|
2265
2298
|
} catch (e) {
|
|
2266
2299
|
try {
|
|
2267
2300
|
cw.onError(e, "routes");
|
|
@@ -2408,6 +2441,7 @@ var RETRY_BUDGET_MS = 2e4;
|
|
|
2408
2441
|
var STATE_ATTEMPTS = 10;
|
|
2409
2442
|
var HISTORY_PAGE = BASELINE_WINDOW + 5;
|
|
2410
2443
|
var HISTORY_MAX = 200;
|
|
2444
|
+
var STRANDED_ERROR = "Imported, but its finish was never written; marked as timed out";
|
|
2411
2445
|
function validateDefinition(def) {
|
|
2412
2446
|
const name = def.name;
|
|
2413
2447
|
if (def.schedule !== void 0) {
|
|
@@ -2584,8 +2618,8 @@ var Cronwatch = class {
|
|
|
2584
2618
|
return json({ ok: false, error: "CRON_SECRET is not set, so this job will not run for an unauthenticated request. Set it, or pass secret: null to handler() to allow anyone." }, 503);
|
|
2585
2619
|
}
|
|
2586
2620
|
if (secret) {
|
|
2587
|
-
const
|
|
2588
|
-
if (!constantTimeEqual(
|
|
2621
|
+
const sent = bearerToken(request.headers.get("authorization"));
|
|
2622
|
+
if (sent === null || !constantTimeEqual(sent, secret)) {
|
|
2589
2623
|
return json({ ok: false, error: "Unauthorized" }, 401);
|
|
2590
2624
|
}
|
|
2591
2625
|
}
|
|
@@ -3126,6 +3160,17 @@ ${id}`;
|
|
|
3126
3160
|
throw new Error(`recordRun: run ids must be 1 to ${MAX_RUN_ID} characters (got ${got}; job "${input.job}")`);
|
|
3127
3161
|
}
|
|
3128
3162
|
if (input.id.includes("\0")) throw new Error(`recordRun: run ids cannot contain a NUL character (job "${input.job}")`);
|
|
3163
|
+
const where = `job "${input.job}", run "${input.id}"`;
|
|
3164
|
+
if (!["running", "ok", "failed", "timeout"].includes(input.status)) {
|
|
3165
|
+
throw new Error(`recordRun: status must be "running", "ok", "failed", or "timeout" (${where})`);
|
|
3166
|
+
}
|
|
3167
|
+
if (!Number.isFinite(input.startedAt)) throw new Error(`recordRun: startedAt must be a finite number (${where})`);
|
|
3168
|
+
if (input.finishedAt !== null && !Number.isFinite(input.finishedAt)) throw new Error(`recordRun: finishedAt must be a finite number or null (${where})`);
|
|
3169
|
+
if (input.durationMs !== null && !Number.isFinite(input.durationMs)) throw new Error(`recordRun: durationMs must be a finite number or null (${where})`);
|
|
3170
|
+
for (const field of ["error", "output"]) {
|
|
3171
|
+
if (input[field] !== null && typeof input[field] !== "string") throw new Error(`recordRun: ${field} must be a string or null (${where})`);
|
|
3172
|
+
}
|
|
3173
|
+
if (typeof input.trigger !== "string") throw new Error(`recordRun: trigger must be a string (${where})`);
|
|
3129
3174
|
for (const [metric, value] of Object.entries(input.metrics ?? {})) {
|
|
3130
3175
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3131
3176
|
throw new Error(`recordRun: metric "${metric}" must be a finite number (job "${input.job}", run "${input.id}")`);
|
|
@@ -3146,7 +3191,7 @@ ${id}`;
|
|
|
3146
3191
|
const definition = toStored(declared);
|
|
3147
3192
|
const stored = await this.store.getRun(run.id);
|
|
3148
3193
|
if (stored) return this.recordOver(definition, stored, run, evaluate);
|
|
3149
|
-
const inserted = evaluate ? { ...run, status: "running" } : run;
|
|
3194
|
+
const inserted = evaluate ? { ...run, status: "running", finishedAt: run.status === "running" ? null : this.now() } : run;
|
|
3150
3195
|
try {
|
|
3151
3196
|
await this.store.insertRun(inserted);
|
|
3152
3197
|
} catch (e) {
|
|
@@ -3271,14 +3316,20 @@ ${id}`;
|
|
|
3271
3316
|
const stored = found ? this.readJob(found) : null;
|
|
3272
3317
|
if (stored) this.evaluable(stored);
|
|
3273
3318
|
const definition = declared ? toStored(declared) : stored?.definition;
|
|
3274
|
-
|
|
3319
|
+
const stranded = isStranded(listed, now);
|
|
3320
|
+
if (!definition || !(stranded || isStuck(definition, listed, now))) continue;
|
|
3275
3321
|
const run = await this.store.getRun(listed.id);
|
|
3276
3322
|
if (!run || run.status !== "running" || run.job !== listed.job) continue;
|
|
3277
|
-
const timeout = timeoutMs(definition);
|
|
3278
3323
|
run.status = "timeout";
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3324
|
+
if (stranded) {
|
|
3325
|
+
run.durationMs ??= runDuration(run.startedAt, run.finishedAt);
|
|
3326
|
+
run.error = STRANDED_ERROR + (run.error === null ? "" : `
|
|
3327
|
+
${run.error}`);
|
|
3328
|
+
} else {
|
|
3329
|
+
run.finishedAt = now;
|
|
3330
|
+
run.durationMs = runDuration(run.startedAt, now);
|
|
3331
|
+
run.error = `Still running after ${formatDuration(timeoutMs(definition))}; marked as timed out`;
|
|
3332
|
+
}
|
|
3282
3333
|
if (!await this.writeRunIf(run, ["running"])) continue;
|
|
3283
3334
|
alerts.push(...await this.finishRun(definition, run, now));
|
|
3284
3335
|
} catch (e) {
|