@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.cjs
CHANGED
|
@@ -724,6 +724,9 @@ function onCheck(def, stored, lastRun, state, now) {
|
|
|
724
724
|
function isStuck(def, run, now) {
|
|
725
725
|
return run.status === "running" && run.finishedAt === null && now - run.startedAt > timeoutMs(def);
|
|
726
726
|
}
|
|
727
|
+
function isStranded(run, now) {
|
|
728
|
+
return run.status === "running" && run.finishedAt !== null && now - run.finishedAt > SEND_LEASE_MS;
|
|
729
|
+
}
|
|
727
730
|
function muteOpens(previous, next) {
|
|
728
731
|
const muted = cloneState(next);
|
|
729
732
|
for (const condition of openConditions(muted)) {
|
|
@@ -899,6 +902,10 @@ function constantTimeEqual(sent, secret) {
|
|
|
899
902
|
for (let i = 0; i < sent.length; i++) diff |= sent.charCodeAt(i) ^ secret.charCodeAt(i % secret.length);
|
|
900
903
|
return diff === 0;
|
|
901
904
|
}
|
|
905
|
+
function bearerToken(header) {
|
|
906
|
+
const match = header === null ? null : /^Bearer\s+([\s\S]*)$/i.exec(header);
|
|
907
|
+
return match ? match[1] : null;
|
|
908
|
+
}
|
|
902
909
|
|
|
903
910
|
// src/output.ts
|
|
904
911
|
var OUTPUT_CAP = 16 * 1024;
|
|
@@ -992,15 +999,45 @@ function stringify(part) {
|
|
|
992
999
|
return String(part);
|
|
993
1000
|
}
|
|
994
1001
|
}
|
|
1002
|
+
var LineWindow = class {
|
|
1003
|
+
items = [];
|
|
1004
|
+
start = 0;
|
|
1005
|
+
size = 0;
|
|
1006
|
+
get count() {
|
|
1007
|
+
return this.items.length - this.start;
|
|
1008
|
+
}
|
|
1009
|
+
push(line) {
|
|
1010
|
+
this.items.push(line);
|
|
1011
|
+
this.size += line.length + 1;
|
|
1012
|
+
}
|
|
1013
|
+
/** Drops the oldest lines while over `max`, always keeping the newest. */
|
|
1014
|
+
keepAtMost(max) {
|
|
1015
|
+
while (this.size > max && this.count > 1) this.drop();
|
|
1016
|
+
}
|
|
1017
|
+
/** Drops the oldest lines only while what is left still holds `min`. */
|
|
1018
|
+
keepAtLeast(min) {
|
|
1019
|
+
while (this.count > 1 && this.size - (this.items[this.start].length + 1) > min) this.drop();
|
|
1020
|
+
}
|
|
1021
|
+
join() {
|
|
1022
|
+
return this.items.slice(this.start).join("\n");
|
|
1023
|
+
}
|
|
1024
|
+
drop() {
|
|
1025
|
+
this.size -= this.items[this.start].length + 1;
|
|
1026
|
+
this.start += 1;
|
|
1027
|
+
if (this.start >= 1024 && this.start * 2 >= this.items.length) {
|
|
1028
|
+
this.items = this.items.slice(this.start);
|
|
1029
|
+
this.start = 0;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
995
1033
|
function createRecorder(run) {
|
|
996
1034
|
const window = OUTPUT_CAP + REDACT_EDGE + 2;
|
|
997
1035
|
const copy = (text) => JSON.parse(JSON.stringify(text));
|
|
998
|
-
const lines =
|
|
999
|
-
let size = 0;
|
|
1036
|
+
const lines = new LineWindow();
|
|
1000
1037
|
const head = [];
|
|
1001
1038
|
let headSize = 0;
|
|
1002
1039
|
let whole = "";
|
|
1003
|
-
|
|
1040
|
+
const raw = new LineWindow();
|
|
1004
1041
|
let logged = false;
|
|
1005
1042
|
const metrics = {};
|
|
1006
1043
|
const controller = new AbortController();
|
|
@@ -1014,7 +1051,8 @@ function createRecorder(run) {
|
|
|
1014
1051
|
const separator = logged ? "\n" : "";
|
|
1015
1052
|
logged = true;
|
|
1016
1053
|
if (whole !== null) whole = whole.length + separator.length + line.length <= 2 * OUTPUT_CAP ? whole + separator + line : null;
|
|
1017
|
-
|
|
1054
|
+
raw.push(copy(line.slice(-OUTPUT_CAP)));
|
|
1055
|
+
raw.keepAtLeast(OUTPUT_CAP);
|
|
1018
1056
|
if (headSize < OUTPUT_CAP) {
|
|
1019
1057
|
head.push(copy(line.slice(0, OUTPUT_CAP - headSize)));
|
|
1020
1058
|
headSize += line.length + 1;
|
|
@@ -1024,10 +1062,7 @@ function createRecorder(run) {
|
|
|
1024
1062
|
line = copy(line.slice(-68609));
|
|
1025
1063
|
}
|
|
1026
1064
|
lines.push(line);
|
|
1027
|
-
|
|
1028
|
-
while (size > window && lines.length > 1) {
|
|
1029
|
-
size -= lines.shift().length + 1;
|
|
1030
|
-
}
|
|
1065
|
+
lines.keepAtMost(window);
|
|
1031
1066
|
},
|
|
1032
1067
|
metric(name, value) {
|
|
1033
1068
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -1041,10 +1076,10 @@ function createRecorder(run) {
|
|
|
1041
1076
|
};
|
|
1042
1077
|
return {
|
|
1043
1078
|
context,
|
|
1044
|
-
output: () => lines.
|
|
1079
|
+
output: () => lines.count === 0 ? null : lines.join(),
|
|
1045
1080
|
expectText: () => {
|
|
1046
1081
|
if (!logged) return null;
|
|
1047
|
-
return whole ?? head.join("\n").slice(0, OUTPUT_CAP) + "\n" +
|
|
1082
|
+
return whole ?? head.join("\n").slice(0, OUTPUT_CAP) + "\n" + raw.join().slice(-OUTPUT_CAP);
|
|
1048
1083
|
},
|
|
1049
1084
|
metrics: () => ({ ...metrics }),
|
|
1050
1085
|
abort: () => controller.abort()
|
|
@@ -1218,7 +1253,7 @@ function staticAsset(path, base) {
|
|
|
1218
1253
|
}
|
|
1219
1254
|
|
|
1220
1255
|
// src/version.ts
|
|
1221
|
-
var VERSION = "0.14.
|
|
1256
|
+
var VERSION = "0.14.3";
|
|
1222
1257
|
var API_VERSION = 1;
|
|
1223
1258
|
|
|
1224
1259
|
// src/routes/timeline.ts
|
|
@@ -1985,10 +2020,6 @@ function forwardedOrigin(request, url) {
|
|
|
1985
2020
|
return url.origin;
|
|
1986
2021
|
}
|
|
1987
2022
|
}
|
|
1988
|
-
function bearerToken(header) {
|
|
1989
|
-
const match = header === null ? null : /^Bearer\s+([\s\S]*)$/i.exec(header);
|
|
1990
|
-
return match ? match[1] : null;
|
|
1991
|
-
}
|
|
1992
2023
|
function signInReturn(referer, publicOrigin, base) {
|
|
1993
2024
|
if (referer === null || !referer.startsWith(publicOrigin + "/")) return `${base}/`;
|
|
1994
2025
|
let url;
|
|
@@ -2157,13 +2188,14 @@ function buildRoutes(cw, options = {}) {
|
|
|
2157
2188
|
return wantsHtml ? html(messagePage("Bad request", "The path is not valid.", base), 400) : api({ ok: false, error: "Bad path" }, 400);
|
|
2158
2189
|
}
|
|
2159
2190
|
const parts = decoded;
|
|
2160
|
-
|
|
2191
|
+
const reads = method === "GET" || method === "HEAD";
|
|
2192
|
+
if (reads && path === "/") {
|
|
2161
2193
|
const entries = await cw.jobsWithRuns(BOARD_PAGE_RUNS);
|
|
2162
2194
|
const now = cw.now();
|
|
2163
2195
|
const runsByJob = new Map(entries.map((entry) => [entry.job.name, entry.runs]));
|
|
2164
2196
|
return html(dashboardPage(entries.map((entry) => entry.job), runsByJob, now, base, null, await boardLanes(cw, entries, now)));
|
|
2165
2197
|
}
|
|
2166
|
-
if (
|
|
2198
|
+
if (reads && parts[0] === "jobs" && parts.length === 2) {
|
|
2167
2199
|
const job = await cw.jobSummary(parts[1]);
|
|
2168
2200
|
if (!job) return html(messagePage("No such job", `${parts[1]} is not in the store.`, base), 404);
|
|
2169
2201
|
const now = cw.now();
|
|
@@ -2199,15 +2231,15 @@ function buildRoutes(cw, options = {}) {
|
|
|
2199
2231
|
}
|
|
2200
2232
|
if (parts[0] === "api") {
|
|
2201
2233
|
const rest = parts.slice(1);
|
|
2202
|
-
if (
|
|
2234
|
+
if (reads && rest.length === 0) {
|
|
2203
2235
|
return api({ ok: true, library: LIBRARY, language: LANGUAGE, version: VERSION, api: API_VERSION });
|
|
2204
2236
|
}
|
|
2205
|
-
if (
|
|
2237
|
+
if (reads && rest[0] === "jobs" && rest.length === 1) {
|
|
2206
2238
|
return api({ ok: true, jobs: await cw.jobs() });
|
|
2207
2239
|
}
|
|
2208
2240
|
if (rest[0] === "jobs" && rest.length === 2) {
|
|
2209
2241
|
const name = rest[1];
|
|
2210
|
-
if (
|
|
2242
|
+
if (reads) {
|
|
2211
2243
|
const job = await cw.jobSummary(name);
|
|
2212
2244
|
if (!job) return api({ ok: false, error: "No such job" }, 404);
|
|
2213
2245
|
return api({ ok: true, job, runs: await cw.runs(name, runsLimit(url.searchParams.get("runs"))) });
|
|
@@ -2248,7 +2280,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
2248
2280
|
return api({ ok: true, ...result });
|
|
2249
2281
|
}
|
|
2250
2282
|
}
|
|
2251
|
-
if (
|
|
2283
|
+
if (reads && rest[0] === "runs" && rest.length === 2) {
|
|
2252
2284
|
const run = await cw.getRun(rest[1]);
|
|
2253
2285
|
return run ? api({ ok: true, run }) : api({ ok: false, error: "No such run" }, 404);
|
|
2254
2286
|
}
|
|
@@ -2263,7 +2295,8 @@ function buildRoutes(cw, options = {}) {
|
|
|
2263
2295
|
const path = stripBase(url.pathname, base);
|
|
2264
2296
|
if (path === null) return html(messagePage("Not found", url.pathname, base), 404);
|
|
2265
2297
|
wantsHtml = !path.startsWith("/api");
|
|
2266
|
-
|
|
2298
|
+
const response = await serve(request, url, path, wantsHtml);
|
|
2299
|
+
return request.method.toUpperCase() === "HEAD" ? new Response(null, { status: response.status, headers: response.headers }) : response;
|
|
2267
2300
|
} catch (e) {
|
|
2268
2301
|
try {
|
|
2269
2302
|
cw.onError(e, "routes");
|
|
@@ -2410,6 +2443,7 @@ var RETRY_BUDGET_MS = 2e4;
|
|
|
2410
2443
|
var STATE_ATTEMPTS = 10;
|
|
2411
2444
|
var HISTORY_PAGE = BASELINE_WINDOW + 5;
|
|
2412
2445
|
var HISTORY_MAX = 200;
|
|
2446
|
+
var STRANDED_ERROR = "Imported, but its finish was never written; marked as timed out";
|
|
2413
2447
|
function validateDefinition(def) {
|
|
2414
2448
|
const name = def.name;
|
|
2415
2449
|
if (def.schedule !== void 0) {
|
|
@@ -2586,8 +2620,8 @@ var Cronwatch = class {
|
|
|
2586
2620
|
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);
|
|
2587
2621
|
}
|
|
2588
2622
|
if (secret) {
|
|
2589
|
-
const
|
|
2590
|
-
if (!constantTimeEqual(
|
|
2623
|
+
const sent = bearerToken(request.headers.get("authorization"));
|
|
2624
|
+
if (sent === null || !constantTimeEqual(sent, secret)) {
|
|
2591
2625
|
return json({ ok: false, error: "Unauthorized" }, 401);
|
|
2592
2626
|
}
|
|
2593
2627
|
}
|
|
@@ -3128,6 +3162,17 @@ ${id}`;
|
|
|
3128
3162
|
throw new Error(`recordRun: run ids must be 1 to ${MAX_RUN_ID} characters (got ${got}; job "${input.job}")`);
|
|
3129
3163
|
}
|
|
3130
3164
|
if (input.id.includes("\0")) throw new Error(`recordRun: run ids cannot contain a NUL character (job "${input.job}")`);
|
|
3165
|
+
const where = `job "${input.job}", run "${input.id}"`;
|
|
3166
|
+
if (!["running", "ok", "failed", "timeout"].includes(input.status)) {
|
|
3167
|
+
throw new Error(`recordRun: status must be "running", "ok", "failed", or "timeout" (${where})`);
|
|
3168
|
+
}
|
|
3169
|
+
if (!Number.isFinite(input.startedAt)) throw new Error(`recordRun: startedAt must be a finite number (${where})`);
|
|
3170
|
+
if (input.finishedAt !== null && !Number.isFinite(input.finishedAt)) throw new Error(`recordRun: finishedAt must be a finite number or null (${where})`);
|
|
3171
|
+
if (input.durationMs !== null && !Number.isFinite(input.durationMs)) throw new Error(`recordRun: durationMs must be a finite number or null (${where})`);
|
|
3172
|
+
for (const field of ["error", "output"]) {
|
|
3173
|
+
if (input[field] !== null && typeof input[field] !== "string") throw new Error(`recordRun: ${field} must be a string or null (${where})`);
|
|
3174
|
+
}
|
|
3175
|
+
if (typeof input.trigger !== "string") throw new Error(`recordRun: trigger must be a string (${where})`);
|
|
3131
3176
|
for (const [metric, value] of Object.entries(input.metrics ?? {})) {
|
|
3132
3177
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3133
3178
|
throw new Error(`recordRun: metric "${metric}" must be a finite number (job "${input.job}", run "${input.id}")`);
|
|
@@ -3148,7 +3193,7 @@ ${id}`;
|
|
|
3148
3193
|
const definition = toStored(declared);
|
|
3149
3194
|
const stored = await this.store.getRun(run.id);
|
|
3150
3195
|
if (stored) return this.recordOver(definition, stored, run, evaluate);
|
|
3151
|
-
const inserted = evaluate ? { ...run, status: "running" } : run;
|
|
3196
|
+
const inserted = evaluate ? { ...run, status: "running", finishedAt: run.status === "running" ? null : this.now() } : run;
|
|
3152
3197
|
try {
|
|
3153
3198
|
await this.store.insertRun(inserted);
|
|
3154
3199
|
} catch (e) {
|
|
@@ -3273,14 +3318,20 @@ ${id}`;
|
|
|
3273
3318
|
const stored = found ? this.readJob(found) : null;
|
|
3274
3319
|
if (stored) this.evaluable(stored);
|
|
3275
3320
|
const definition = declared ? toStored(declared) : stored?.definition;
|
|
3276
|
-
|
|
3321
|
+
const stranded = isStranded(listed, now);
|
|
3322
|
+
if (!definition || !(stranded || isStuck(definition, listed, now))) continue;
|
|
3277
3323
|
const run = await this.store.getRun(listed.id);
|
|
3278
3324
|
if (!run || run.status !== "running" || run.job !== listed.job) continue;
|
|
3279
|
-
const timeout = timeoutMs(definition);
|
|
3280
3325
|
run.status = "timeout";
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3326
|
+
if (stranded) {
|
|
3327
|
+
run.durationMs ??= runDuration(run.startedAt, run.finishedAt);
|
|
3328
|
+
run.error = STRANDED_ERROR + (run.error === null ? "" : `
|
|
3329
|
+
${run.error}`);
|
|
3330
|
+
} else {
|
|
3331
|
+
run.finishedAt = now;
|
|
3332
|
+
run.durationMs = runDuration(run.startedAt, now);
|
|
3333
|
+
run.error = `Still running after ${formatDuration(timeoutMs(definition))}; marked as timed out`;
|
|
3334
|
+
}
|
|
3284
3335
|
if (!await this.writeRunIf(run, ["running"])) continue;
|
|
3285
3336
|
alerts.push(...await this.finishRun(definition, run, now));
|
|
3286
3337
|
} catch (e) {
|