@cronwatch/sdk 0.13.0 → 0.14.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/dist/anthropic.cjs +3 -3
- 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 +3 -3
- package/dist/anthropic.js.map +1 -1
- package/dist/bugsnag.cjs +28 -5
- 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 +28 -5
- package/dist/bugsnag.js.map +1 -1
- package/dist/{client-DAJH7YiG.d.ts → client-C8j7B2Bo.d.ts} +31 -19
- package/dist/{client-z1LdR_m2.d.cts → client-CHjrNHH-.d.cts} +31 -19
- package/dist/d1.cjs.map +1 -1
- package/dist/d1.d.cts +2 -2
- package/dist/d1.d.ts +2 -2
- package/dist/d1.js.map +1 -1
- package/dist/datadog.cjs +28 -5
- package/dist/datadog.cjs.map +1 -1
- package/dist/datadog.d.cts +1 -1
- package/dist/datadog.d.ts +1 -1
- package/dist/datadog.js +28 -5
- package/dist/datadog.js.map +1 -1
- package/dist/discord.cjs +28 -1
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +1 -1
- package/dist/discord.d.ts +1 -1
- package/dist/discord.js +28 -1
- package/dist/discord.js.map +1 -1
- package/dist/{email-CCUzlLe9.d.cts → email-D7SfsU0F.d.ts} +2 -2
- package/dist/{email-CFtDqlvZ.d.ts → email-zSazJClH.d.cts} +2 -2
- package/dist/honeybadger.cjs +28 -5
- 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 +28 -5
- package/dist/honeybadger.js.map +1 -1
- package/dist/index.cjs +249 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -7
- package/dist/index.d.ts +11 -7
- package/dist/index.js +249 -84
- package/dist/index.js.map +1 -1
- package/dist/mailgun.cjs +28 -5
- 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 +28 -5
- package/dist/mailgun.js.map +1 -1
- package/dist/newrelic.cjs +28 -5
- 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 +28 -5
- package/dist/newrelic.js.map +1 -1
- package/dist/pg-cron.cjs.map +1 -1
- package/dist/pg-cron.d.cts +2 -2
- package/dist/pg-cron.d.ts +2 -2
- package/dist/pg-cron.js.map +1 -1
- package/dist/postgres.cjs.map +1 -1
- package/dist/postgres.d.cts +2 -2
- package/dist/postgres.d.ts +2 -2
- package/dist/postgres.js.map +1 -1
- package/dist/postmark.cjs +28 -5
- 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 +28 -5
- package/dist/postmark.js.map +1 -1
- package/dist/resend.cjs +28 -5
- 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 +28 -5
- package/dist/resend.js.map +1 -1
- package/dist/rollbar.cjs +28 -5
- 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 +28 -5
- package/dist/rollbar.js.map +1 -1
- package/dist/sendgrid.cjs +28 -5
- 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 +28 -5
- package/dist/sendgrid.js.map +1 -1
- package/dist/sentry.cjs +28 -5
- package/dist/sentry.cjs.map +1 -1
- package/dist/sentry.d.cts +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.js +28 -5
- package/dist/sentry.js.map +1 -1
- package/dist/ses.cjs +28 -5
- 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 +28 -5
- package/dist/ses.js.map +1 -1
- package/dist/slack.cjs +32 -2
- package/dist/slack.cjs.map +1 -1
- package/dist/slack.d.cts +6 -2
- package/dist/slack.d.ts +6 -2
- package/dist/slack.js +32 -2
- package/dist/slack.js.map +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.map +1 -1
- package/dist/twilio.cjs +28 -5
- package/dist/twilio.cjs.map +1 -1
- package/dist/twilio.d.cts +1 -1
- package/dist/twilio.d.ts +1 -1
- package/dist/twilio.js +28 -5
- package/dist/twilio.js.map +1 -1
- package/dist/{types-D6t0Z846.d.cts → types-B8IFVK5k.d.cts} +23 -7
- package/dist/{types-D6t0Z846.d.ts → types-B8IFVK5k.d.ts} +23 -7
- package/dist/webhook.cjs +10 -2
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +24 -6
- package/dist/webhook.d.ts +24 -6
- package/dist/webhook.js +10 -2
- package/dist/webhook.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -172,6 +172,13 @@ function countFrom(from) {
|
|
|
172
172
|
return from >= FIRST_DATE_MS ? from : FIRST_DATE_MS - 1;
|
|
173
173
|
}
|
|
174
174
|
function fireAfter(parsed, from) {
|
|
175
|
+
const found = croneFireAfter(parsed, from);
|
|
176
|
+
const start = countFrom(from);
|
|
177
|
+
if (start === null) return found;
|
|
178
|
+
const moved = gapFireAfter(parsed, start);
|
|
179
|
+
return moved !== null && (found === null || moved < found) ? moved : found;
|
|
180
|
+
}
|
|
181
|
+
function croneFireAfter(parsed, from) {
|
|
175
182
|
const cron = crons.get(parsed);
|
|
176
183
|
if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
|
|
177
184
|
if (never.has(parsed)) return null;
|
|
@@ -187,6 +194,43 @@ function fireAfter(parsed, from) {
|
|
|
187
194
|
}
|
|
188
195
|
return null;
|
|
189
196
|
}
|
|
197
|
+
function springGap(timezone, at) {
|
|
198
|
+
const jump = springJump(timezone, at);
|
|
199
|
+
return jump && at < jump.jumpAt + jump.gapMs ? jump : null;
|
|
200
|
+
}
|
|
201
|
+
var JUMP_LOOKBACK_MS = 3 * 36e5;
|
|
202
|
+
function springJump(timezone, at) {
|
|
203
|
+
if (at - JUMP_LOOKBACK_MS < FIRST_DATE_MS || at >= LAST_DATE_MS) return null;
|
|
204
|
+
const after2 = utcOffset(at, timezone);
|
|
205
|
+
const before = utcOffset(at - JUMP_LOOKBACK_MS, timezone);
|
|
206
|
+
if (after2 <= before) return null;
|
|
207
|
+
let lo = at - JUMP_LOOKBACK_MS;
|
|
208
|
+
let hi = at;
|
|
209
|
+
while (hi - lo > 6e4) {
|
|
210
|
+
const mid = lo + Math.floor((hi - lo) / 2);
|
|
211
|
+
if (utcOffset(mid, timezone) === after2) hi = mid;
|
|
212
|
+
else lo = mid;
|
|
213
|
+
}
|
|
214
|
+
return { jumpAt: Math.floor(hi / 6e4) * 6e4, gapMs: after2 - before, beforeMs: before };
|
|
215
|
+
}
|
|
216
|
+
var wallCrons = /* @__PURE__ */ new WeakMap();
|
|
217
|
+
function wallCron(parsed) {
|
|
218
|
+
let wall = wallCrons.get(parsed);
|
|
219
|
+
if (!wall) {
|
|
220
|
+
wall = new Cron(parsed.source, { timezone: "UTC" });
|
|
221
|
+
wallCrons.set(parsed, wall);
|
|
222
|
+
}
|
|
223
|
+
return wall;
|
|
224
|
+
}
|
|
225
|
+
function gapFireAfter(parsed, start) {
|
|
226
|
+
if (never.has(parsed)) return null;
|
|
227
|
+
const gap = springGap(parsed.timezone, start);
|
|
228
|
+
if (!gap) return null;
|
|
229
|
+
const wall = wallCron(parsed);
|
|
230
|
+
const [slot] = runsAfter(wall, 1, start + gap.beforeMs);
|
|
231
|
+
if (slot === void 0 || slot >= gap.jumpAt + gap.beforeMs + gap.gapMs) return null;
|
|
232
|
+
return slot - gap.beforeMs;
|
|
233
|
+
}
|
|
190
234
|
function firesBetween(parsed, from, to, limit) {
|
|
191
235
|
const cron = crons.get(parsed);
|
|
192
236
|
if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
|
|
@@ -197,15 +241,34 @@ function firesBetween(parsed, from, to, limit) {
|
|
|
197
241
|
let probe = start;
|
|
198
242
|
let last = start;
|
|
199
243
|
for (let guard = 0; guard < 1e3; guard++) {
|
|
244
|
+
if (springGap(parsed.timezone, last)) {
|
|
245
|
+
const t = fireAfter(parsed, last);
|
|
246
|
+
if (t === null || t > to) return out;
|
|
247
|
+
out.push(t);
|
|
248
|
+
last = probe = t;
|
|
249
|
+
if (out.length > limit) return null;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
200
252
|
const batch = runsAfter(cron, Math.min(limit + 1 - out.length, 24), probe);
|
|
201
253
|
if (batch.length === 0) return out;
|
|
254
|
+
let stepped = false;
|
|
202
255
|
for (const t of batch) {
|
|
203
256
|
if (t <= last) continue;
|
|
257
|
+
if (springGap(parsed.timezone, t)) {
|
|
258
|
+
const next = fireAfter(parsed, last);
|
|
259
|
+
if (next === null || next > to) return out;
|
|
260
|
+
out.push(next);
|
|
261
|
+
last = probe = next;
|
|
262
|
+
if (out.length > limit) return null;
|
|
263
|
+
stepped = true;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
204
266
|
if (t > to) return out;
|
|
205
267
|
out.push(t);
|
|
206
268
|
last = t;
|
|
207
269
|
if (out.length > limit) return null;
|
|
208
270
|
}
|
|
271
|
+
if (stepped) continue;
|
|
209
272
|
const end = batch[batch.length - 1];
|
|
210
273
|
probe = end > probe ? end : probe + 36e5;
|
|
211
274
|
}
|
|
@@ -218,10 +281,14 @@ function nextFire(parsed, from, lastRunAt) {
|
|
|
218
281
|
}
|
|
219
282
|
return fireAfter(parsed, from);
|
|
220
283
|
}
|
|
221
|
-
function
|
|
284
|
+
function intervalDue(everyMs, startedAt, finishedAt) {
|
|
285
|
+
const due = startedAt + everyMs;
|
|
286
|
+
return finishedAt !== null && finishedAt > due ? finishedAt + everyMs : due;
|
|
287
|
+
}
|
|
288
|
+
function expectation(parsed, lastRunAt, registeredAt, graceMs2, lastFinishedAt = null) {
|
|
222
289
|
let dueAt;
|
|
223
290
|
if (parsed.kind === "interval") {
|
|
224
|
-
dueAt =
|
|
291
|
+
dueAt = lastRunAt === null ? registeredAt + parsed.everyMs : intervalDue(parsed.everyMs, lastRunAt, lastFinishedAt);
|
|
225
292
|
} else if (lastRunAt === null) {
|
|
226
293
|
dueAt = fireAfter(parsed, registeredAt - 1);
|
|
227
294
|
} else {
|
|
@@ -230,33 +297,25 @@ function expectation(parsed, lastRunAt, registeredAt, graceMs2) {
|
|
|
230
297
|
return dueAt === null ? null : { dueAt, deadline: dueAt + graceMs2 };
|
|
231
298
|
}
|
|
232
299
|
function dueAfterRun(parsed, startedAt) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
300
|
+
let next = fireAfter(parsed, startedAt);
|
|
301
|
+
for (let guard = 0; next !== null && guard < 1e5; guard++) {
|
|
302
|
+
const following = fireAfter(parsed, next);
|
|
303
|
+
const covers = runCovers(startedAt, next, following) || inSpringForwardGap(parsed, startedAt, next);
|
|
304
|
+
if (!covers) return next;
|
|
305
|
+
next = following;
|
|
306
|
+
}
|
|
307
|
+
return next;
|
|
238
308
|
}
|
|
239
309
|
function runCovers(startedAt, dueAt, followingAt = null) {
|
|
240
310
|
const slack = followingAt === null ? EARLY_SLACK_MS : Math.min(EARLY_SLACK_MS, Math.floor((followingAt - dueAt) / 2));
|
|
241
311
|
return startedAt >= dueAt - slack;
|
|
242
312
|
}
|
|
243
313
|
function inSpringForwardGap(parsed, startedAt, fireAt) {
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
if (gap <= 0) return false;
|
|
250
|
-
let lo = fireAt - LOOKBACK;
|
|
251
|
-
let hi = fireAt;
|
|
252
|
-
while (hi - lo > 6e4) {
|
|
253
|
-
const mid = lo + Math.floor((hi - lo) / 2);
|
|
254
|
-
if (utcOffset(mid, parsed.timezone) === after2) hi = mid;
|
|
255
|
-
else lo = mid;
|
|
256
|
-
}
|
|
257
|
-
const jumpAt = Math.floor(hi / 6e4) * 6e4;
|
|
258
|
-
if (fireAt - jumpAt >= gap || startedAt < jumpAt - EARLY_SLACK_MS || startedAt >= fireAt) return false;
|
|
259
|
-
return fireAfter(parsed, jumpAt - 1) === fireAt;
|
|
314
|
+
const gap = springGap(parsed.timezone, fireAt);
|
|
315
|
+
if (!gap || startedAt < gap.jumpAt - EARLY_SLACK_MS || startedAt >= fireAt) return false;
|
|
316
|
+
const wall = wallCron(parsed);
|
|
317
|
+
const fires = (at) => runsAfter(wall, 1, at - 1)[0] === at;
|
|
318
|
+
return fires(fireAt + gap.beforeMs) && !fires(fireAt + gap.beforeMs + gap.gapMs);
|
|
260
319
|
}
|
|
261
320
|
var formatters = /* @__PURE__ */ new Map();
|
|
262
321
|
function utcOffset(at, timezone) {
|
|
@@ -353,15 +412,75 @@ var SEND_LEASE_MS = 5 * 6e4;
|
|
|
353
412
|
function alertKey(alert) {
|
|
354
413
|
return `${alert.type}|${alert.at}|${alert.run?.id ?? ""}`;
|
|
355
414
|
}
|
|
415
|
+
var HELD_TEXT_CAP = 2048;
|
|
416
|
+
var HELD_TEXT_SLACK = 64;
|
|
417
|
+
function heldRun(run) {
|
|
418
|
+
const error = typeof run.error === "string" && run.error.length > HELD_TEXT_CAP + HELD_TEXT_SLACK ? keepHead(run.error) : run.error;
|
|
419
|
+
const output = typeof run.output === "string" && run.output.length > HELD_TEXT_CAP + HELD_TEXT_SLACK ? keepTail(run.output) : run.output;
|
|
420
|
+
return error === run.error && output === run.output ? run : { ...run, error, output };
|
|
421
|
+
}
|
|
422
|
+
var HELD_MESSAGE_CAP = 4096;
|
|
423
|
+
function keepHead(text, cap = HELD_TEXT_CAP) {
|
|
424
|
+
let end = cap;
|
|
425
|
+
if (isHighSurrogate(text.charCodeAt(end - 1))) end -= 1;
|
|
426
|
+
return `${text.slice(0, end)}
|
|
427
|
+
[${text.length - end} more characters trimmed]`;
|
|
428
|
+
}
|
|
429
|
+
function keepTail(text) {
|
|
430
|
+
let start = text.length - HELD_TEXT_CAP;
|
|
431
|
+
if (isLowSurrogate(text.charCodeAt(start)) && isHighSurrogate(text.charCodeAt(start - 1))) start += 1;
|
|
432
|
+
return `[${start} earlier characters trimmed]
|
|
433
|
+
${text.slice(start)}`;
|
|
434
|
+
}
|
|
435
|
+
function isHighSurrogate(code) {
|
|
436
|
+
return code >= 55296 && code <= 56319;
|
|
437
|
+
}
|
|
438
|
+
function isLowSurrogate(code) {
|
|
439
|
+
return code >= 56320 && code <= 57343;
|
|
440
|
+
}
|
|
441
|
+
function heldAlert(alert) {
|
|
442
|
+
const run = alert.run;
|
|
443
|
+
const trimmedRun = isObject(run) ? heldRun(run) : run;
|
|
444
|
+
const message = alert.message;
|
|
445
|
+
const trimmedMessage = typeof message === "string" && message.length > HELD_MESSAGE_CAP + HELD_TEXT_SLACK ? keepHead(message, HELD_MESSAGE_CAP) : message;
|
|
446
|
+
if (trimmedRun === run && trimmedMessage === message) return alert;
|
|
447
|
+
return { ...alert, run: trimmedRun, message: trimmedMessage };
|
|
448
|
+
}
|
|
449
|
+
function channelKeys(names) {
|
|
450
|
+
const seen = /* @__PURE__ */ new Map();
|
|
451
|
+
const taken = /* @__PURE__ */ new Set();
|
|
452
|
+
return names.map((name) => {
|
|
453
|
+
let n = (seen.get(name) ?? 0) + 1;
|
|
454
|
+
seen.set(name, n);
|
|
455
|
+
let key = n === 1 ? name : `${name}#${n}`;
|
|
456
|
+
while (taken.has(key)) key = `${name}#${++n}`;
|
|
457
|
+
taken.add(key);
|
|
458
|
+
return key;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function owedChannels(alert, configured) {
|
|
462
|
+
const owed = alert.owed;
|
|
463
|
+
if (!Array.isArray(owed)) return [...configured];
|
|
464
|
+
return configured.filter((key) => owed.includes(key));
|
|
465
|
+
}
|
|
466
|
+
function afterSend(alert, tried, failed) {
|
|
467
|
+
const owed = tried.filter((key) => failed.includes(key));
|
|
468
|
+
return owed.length === 0 ? null : { ...alert, owed };
|
|
469
|
+
}
|
|
470
|
+
function withoutOwed(alert) {
|
|
471
|
+
if (!("owed" in alert)) return alert;
|
|
472
|
+
const { owed: _owed, ...rest } = alert;
|
|
473
|
+
return rest;
|
|
474
|
+
}
|
|
356
475
|
function newest(list, max) {
|
|
357
476
|
return { kept: list.slice(-max), dropped: Math.max(0, list.length - max) };
|
|
358
477
|
}
|
|
359
478
|
function queueUndelivered(state, alerts) {
|
|
360
479
|
const next = cloneState(state);
|
|
361
|
-
const byKey = new Map(alerts.map((alert) => [alertKey(alert), alert]));
|
|
480
|
+
const byKey = new Map(alerts.map((alert) => [alertKey(alert), heldAlert(alert)]));
|
|
362
481
|
const queue = next.undelivered.map((alert) => byKey.get(alertKey(alert)) ?? alert);
|
|
363
482
|
const known = new Set(queue.map(alertKey));
|
|
364
|
-
queue.push(...
|
|
483
|
+
queue.push(...[...byKey.values()].filter((alert) => !known.has(alertKey(alert))));
|
|
365
484
|
const { kept, dropped } = newest(queue, MAX_UNDELIVERED);
|
|
366
485
|
next.undelivered = kept;
|
|
367
486
|
return { state: next, dropped };
|
|
@@ -370,7 +489,7 @@ function holdAlerts(state, alerts, until, deferred) {
|
|
|
370
489
|
if (alerts.length === 0) return { state, dropped: 0 };
|
|
371
490
|
if (deferred) return queueUndelivered(state, alerts);
|
|
372
491
|
const next = cloneState(state);
|
|
373
|
-
const { kept, dropped } = newest([...next.sending ?? [], ...alerts.map((alert) => ({ until, alert }))], MAX_UNDELIVERED);
|
|
492
|
+
const { kept, dropped } = newest([...next.sending ?? [], ...alerts.map((alert) => ({ until, alert: heldAlert(alert) }))], MAX_UNDELIVERED);
|
|
374
493
|
next.sending = kept;
|
|
375
494
|
return { state: next, dropped };
|
|
376
495
|
}
|
|
@@ -384,7 +503,7 @@ function releaseSending(state, now) {
|
|
|
384
503
|
lapsed.filter((entry) => typeof entry?.alert === "object" && entry.alert !== null).map((entry) => entry.alert)
|
|
385
504
|
);
|
|
386
505
|
}
|
|
387
|
-
function recordSent(state, delivered, failed, stale, now) {
|
|
506
|
+
function recordSent(state, delivered, failed, stale, now, reached = delivered.length > 0) {
|
|
388
507
|
const next = cloneState(state);
|
|
389
508
|
const done = new Set([...delivered, ...stale].map(alertKey));
|
|
390
509
|
next.undelivered = next.undelivered.filter((alert) => !done.has(alertKey(alert)));
|
|
@@ -392,7 +511,7 @@ function recordSent(state, delivered, failed, stale, now) {
|
|
|
392
511
|
const held = (next.sending ?? []).filter((entry) => !(typeof entry?.alert === "object" && entry.alert !== null && sent.has(alertKey(entry.alert))));
|
|
393
512
|
if (held.length > 0) next.sending = held;
|
|
394
513
|
else delete next.sending;
|
|
395
|
-
if (
|
|
514
|
+
if (reached) next.lastAlertAt = now;
|
|
396
515
|
return queueUndelivered(next, failed);
|
|
397
516
|
}
|
|
398
517
|
function claimUndelivered(state, now, until) {
|
|
@@ -500,9 +619,21 @@ function formatNumber(n) {
|
|
|
500
619
|
if (Number.isInteger(n)) return n.toLocaleString("en-US");
|
|
501
620
|
return n.toLocaleString("en-US", { maximumFractionDigits: 4 });
|
|
502
621
|
}
|
|
503
|
-
function
|
|
622
|
+
function finishedAtOf(run) {
|
|
623
|
+
return run.status !== "running" && typeof run.finishedAt === "number" ? run.finishedAt : null;
|
|
624
|
+
}
|
|
625
|
+
function catchesUp(def, run, now) {
|
|
626
|
+
if (!def.schedule) return true;
|
|
627
|
+
try {
|
|
628
|
+
const exp = expectation(parseSchedule(def.schedule, def.timezone), run.startedAt, run.startedAt, graceMs(def), finishedAtOf(run));
|
|
629
|
+
return exp === null || now <= exp.deadline;
|
|
630
|
+
} catch {
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function onRunStart(def, run, state, now) {
|
|
504
635
|
const next = cloneState(state);
|
|
505
|
-
closeCondition(next, "missed");
|
|
636
|
+
if (catchesUp(def, run, now)) closeCondition(next, "missed");
|
|
506
637
|
closeCondition(next, "stuck");
|
|
507
638
|
return next;
|
|
508
639
|
}
|
|
@@ -511,7 +642,7 @@ function onRunFinish(def, run, state, history, now) {
|
|
|
511
642
|
const alerts = [];
|
|
512
643
|
if (run.status === "ok") {
|
|
513
644
|
next.consecutiveFailures = 0;
|
|
514
|
-
closeCondition(next, "missed");
|
|
645
|
+
if (catchesUp(def, run, now)) closeCondition(next, "missed");
|
|
515
646
|
closeCondition(next, "stuck");
|
|
516
647
|
closeCondition(next, "failed");
|
|
517
648
|
const slow = slowThreshold(def, history);
|
|
@@ -548,7 +679,7 @@ function onRunFinish(def, run, state, history, now) {
|
|
|
548
679
|
return { state: next, alerts };
|
|
549
680
|
}
|
|
550
681
|
next.consecutiveFailures = Math.min(next.consecutiveFailures + 1, MAX_DURATION_MS);
|
|
551
|
-
closeCondition(next, "missed");
|
|
682
|
+
if (catchesUp(def, run, now)) closeCondition(next, "missed");
|
|
552
683
|
const threshold = Math.max(1, def.failuresBeforeAlert ?? 1);
|
|
553
684
|
const condition = run.status === "timeout" ? "stuck" : "failed";
|
|
554
685
|
if (next.consecutiveFailures >= threshold) {
|
|
@@ -573,8 +704,8 @@ function onCheck(def, stored, lastRun, state, now) {
|
|
|
573
704
|
const parsed = parseSchedule(def.schedule, def.timezone);
|
|
574
705
|
const grace = graceMs(def);
|
|
575
706
|
const lastRunAt = lastRun?.startedAt ?? null;
|
|
576
|
-
const exp = expectation(parsed, lastRunAt, stored.createdAt, grace);
|
|
577
|
-
const nextExpectedAt = parsed.kind === "interval" ?
|
|
707
|
+
const exp = expectation(parsed, lastRunAt, stored.createdAt, grace, lastRun ? finishedAtOf(lastRun) : null);
|
|
708
|
+
const nextExpectedAt = parsed.kind === "interval" ? exp.dueAt : nextFire(parsed, now, null);
|
|
578
709
|
if (!exp) return { state: next, alerts, nextExpectedAt, dueAt: null };
|
|
579
710
|
if (parsed.kind === "interval" && lastRun?.status === "running") {
|
|
580
711
|
return { state: next, alerts, nextExpectedAt, dueAt: exp.dueAt };
|
|
@@ -1076,7 +1207,7 @@ function staticAsset(path, base) {
|
|
|
1076
1207
|
}
|
|
1077
1208
|
|
|
1078
1209
|
// src/version.ts
|
|
1079
|
-
var VERSION = "0.
|
|
1210
|
+
var VERSION = "0.14.0";
|
|
1080
1211
|
var API_VERSION = 1;
|
|
1081
1212
|
|
|
1082
1213
|
// src/routes/timeline.ts
|
|
@@ -1129,13 +1260,13 @@ function dueTimes(job, parsed, runs, from, to) {
|
|
|
1129
1260
|
if (parsed.kind === "interval") {
|
|
1130
1261
|
const every = parsed.everyMs;
|
|
1131
1262
|
if ((to - from) / every > MAX_TICKS) return { times: [], dense: true };
|
|
1132
|
-
const
|
|
1263
|
+
const ordered = [...runs].sort((a, b) => a.startedAt - b.startedAt);
|
|
1264
|
+
const dues = ordered.map((r) => intervalDue(every, r.startedAt, finishedAtOf(r)));
|
|
1133
1265
|
const times2 = /* @__PURE__ */ new Set();
|
|
1134
|
-
for (const
|
|
1135
|
-
const t2 = start + every;
|
|
1266
|
+
for (const t2 of dues) {
|
|
1136
1267
|
if (t2 >= from && t2 <= to) times2.add(t2);
|
|
1137
1268
|
}
|
|
1138
|
-
let t =
|
|
1269
|
+
let t = dues.length ? dues[dues.length - 1] : job.nextExpectedAt;
|
|
1139
1270
|
if (t !== null) {
|
|
1140
1271
|
if (t < from) t += Math.ceil((from - t) / every) * every;
|
|
1141
1272
|
for (; t <= to; t += every) times2.add(t);
|
|
@@ -1148,8 +1279,8 @@ function dueTimes(job, parsed, runs, from, to) {
|
|
|
1148
1279
|
function missedAt(job, parsed, times, now) {
|
|
1149
1280
|
if (!parsed || !job.open.includes("missed")) return null;
|
|
1150
1281
|
const grace = safely(() => graceMs(job.definition), 0);
|
|
1151
|
-
const
|
|
1152
|
-
if (
|
|
1282
|
+
const lastRun = job.lastRun;
|
|
1283
|
+
if (lastRun) return safely(() => expectation(parsed, lastRun.startedAt, lastRun.startedAt, grace, finishedAtOf(lastRun))?.dueAt ?? null, null);
|
|
1153
1284
|
const past = times.filter((t) => t + grace < now);
|
|
1154
1285
|
return past.length ? past[past.length - 1] : null;
|
|
1155
1286
|
}
|
|
@@ -1858,7 +1989,8 @@ function signInReturn(referer, publicOrigin, base) {
|
|
|
1858
1989
|
return url.searchParams.has("token") ? `${base}/` : referer;
|
|
1859
1990
|
}
|
|
1860
1991
|
function stripBase(pathname, base) {
|
|
1861
|
-
|
|
1992
|
+
if (base !== "" && pathname !== base && !pathname.startsWith(`${base}/`)) return null;
|
|
1993
|
+
let path = pathname.slice(base.length);
|
|
1862
1994
|
if (path === "") path = "/";
|
|
1863
1995
|
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
|
1864
1996
|
return path;
|
|
@@ -2114,6 +2246,7 @@ function buildRoutes(cw, options = {}) {
|
|
|
2114
2246
|
try {
|
|
2115
2247
|
const url = new URL(request.url);
|
|
2116
2248
|
const path = stripBase(url.pathname, base);
|
|
2249
|
+
if (path === null) return html(messagePage("Not found", url.pathname, base), 404);
|
|
2117
2250
|
wantsHtml = !path.startsWith("/api");
|
|
2118
2251
|
return await serve(request, url, path, wantsHtml);
|
|
2119
2252
|
} catch (e) {
|
|
@@ -2468,7 +2601,7 @@ var Cronwatch = class {
|
|
|
2468
2601
|
warnNoSecret() {
|
|
2469
2602
|
if (this.warnedNoSecret) return;
|
|
2470
2603
|
this.warnedNoSecret = true;
|
|
2471
|
-
this.
|
|
2604
|
+
this.report(new Error("handler() refused a request because no CRON_SECRET is set; pass secret: null to allow unauthenticated requests"), "handler");
|
|
2472
2605
|
}
|
|
2473
2606
|
/** onError, for places that must carry on even when onError itself throws. */
|
|
2474
2607
|
report(error, where) {
|
|
@@ -2481,6 +2614,13 @@ var Cronwatch = class {
|
|
|
2481
2614
|
if (!this.ready) {
|
|
2482
2615
|
this.ready = (async () => {
|
|
2483
2616
|
if (this.store.init) await this.store.init();
|
|
2617
|
+
const missing = ["compareAndSetState", "updateRunIf"].filter((method) => typeof this.store[method] !== "function");
|
|
2618
|
+
if (missing.length > 0) {
|
|
2619
|
+
this.report(
|
|
2620
|
+
new Error(`the store has no ${missing.join(" or ")}, so two processes sharing it can overwrite each other's state or both evaluate the same run; it is safe only while one process at a time uses it`),
|
|
2621
|
+
"store"
|
|
2622
|
+
);
|
|
2623
|
+
}
|
|
2484
2624
|
if (this.usingDefaultStore && isProduction()) {
|
|
2485
2625
|
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.");
|
|
2486
2626
|
}
|
|
@@ -2628,10 +2768,11 @@ var Cronwatch = class {
|
|
|
2628
2768
|
await this.store.insertRun({ ...run });
|
|
2629
2769
|
recorded = true;
|
|
2630
2770
|
} catch (e) {
|
|
2631
|
-
this.
|
|
2771
|
+
this.report(e, `recording ${name}`);
|
|
2632
2772
|
}
|
|
2633
|
-
const
|
|
2634
|
-
|
|
2773
|
+
const begun = { ...run };
|
|
2774
|
+
const started = recorded ? this.updateState(name, (before) => ({ state: holdRun(onRunStart(definition, begun, before, this.now()), run.id), result: void 0 })).then(() => {
|
|
2775
|
+
}, (e) => this.report(e, `starting ${name}`)) : Promise.resolve();
|
|
2635
2776
|
const recorder = createRecorder(run);
|
|
2636
2777
|
const clearTimer = after(timeoutMs(definition), () => recorder.abort());
|
|
2637
2778
|
let result;
|
|
@@ -2656,7 +2797,7 @@ var Cronwatch = class {
|
|
|
2656
2797
|
const ignored = await this.recordFinish(definition, run, recorded, finishedAt);
|
|
2657
2798
|
if (ignored) this.report(new Error(`run ${run.id} of ${name} ${ignored}; ignored`), `finishing ${name}`);
|
|
2658
2799
|
} catch (e) {
|
|
2659
|
-
this.
|
|
2800
|
+
this.report(e, `recording ${name}`);
|
|
2660
2801
|
}
|
|
2661
2802
|
return { run, result, error, threw };
|
|
2662
2803
|
}
|
|
@@ -2759,7 +2900,7 @@ ${id}`;
|
|
|
2759
2900
|
await this.ensureReady();
|
|
2760
2901
|
stored = await this.store.getRun(id);
|
|
2761
2902
|
} catch (e) {
|
|
2762
|
-
this.
|
|
2903
|
+
this.report(e, `recording ${name}`);
|
|
2763
2904
|
}
|
|
2764
2905
|
if (stored) return this.existingHandle(definition, stored);
|
|
2765
2906
|
}
|
|
@@ -2783,10 +2924,10 @@ ${id}`;
|
|
|
2783
2924
|
} catch (e) {
|
|
2784
2925
|
const stored = id === void 0 ? null : await this.store.getRun(id).catch(() => null);
|
|
2785
2926
|
if (stored) return this.existingHandle(definition, stored);
|
|
2786
|
-
this.
|
|
2927
|
+
this.report(e, `recording ${name}`);
|
|
2787
2928
|
}
|
|
2788
2929
|
if (recorded) {
|
|
2789
|
-
await this.updateState(name, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 })).catch((e) => this.
|
|
2930
|
+
await this.updateState(name, (before) => ({ state: holdRun(onRunStart(definition, run, before, this.now()), run.id), result: void 0 })).catch((e) => this.report(e, `starting ${name}`));
|
|
2790
2931
|
}
|
|
2791
2932
|
return this.runHandle(definition, run.id, run, recorded, null);
|
|
2792
2933
|
}
|
|
@@ -2798,7 +2939,7 @@ ${id}`;
|
|
|
2798
2939
|
await this.ensureReady();
|
|
2799
2940
|
stored = await this.store.getRun(runId);
|
|
2800
2941
|
} catch (e) {
|
|
2801
|
-
this.
|
|
2942
|
+
this.report(e, `resuming ${definition.name}`);
|
|
2802
2943
|
return this.runHandle(definition, runId, null, true, null);
|
|
2803
2944
|
}
|
|
2804
2945
|
if (!stored) return this.runHandle(definition, runId, null, true, "was not found");
|
|
@@ -2999,7 +3140,7 @@ ${id}`;
|
|
|
2999
3140
|
throw e;
|
|
3000
3141
|
}
|
|
3001
3142
|
if (!evaluate) return [];
|
|
3002
|
-
await this.updateState(run.job, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 }));
|
|
3143
|
+
await this.updateState(run.job, (before) => ({ state: holdRun(onRunStart(definition, run, before, this.now()), run.id), result: void 0 }));
|
|
3003
3144
|
if (run.status === "running") return [];
|
|
3004
3145
|
return this.finishRun(definition, run, this.now());
|
|
3005
3146
|
}
|
|
@@ -3037,7 +3178,7 @@ ${id}`;
|
|
|
3037
3178
|
return this.outbox(applySilence(before, onRunFinish(definition, run, before, history, now), now), definition, now);
|
|
3038
3179
|
}));
|
|
3039
3180
|
} catch (e) {
|
|
3040
|
-
this.
|
|
3181
|
+
this.report(e, `evaluating ${run.job}`);
|
|
3041
3182
|
return [];
|
|
3042
3183
|
}
|
|
3043
3184
|
this.reportDropped(run.job, held.dropped);
|
|
@@ -3076,7 +3217,7 @@ ${id}`;
|
|
|
3076
3217
|
}
|
|
3077
3218
|
/**
|
|
3078
3219
|
* Look for missed and stuck runs across every job, send alerts, retry
|
|
3079
|
-
* alerts
|
|
3220
|
+
* alerts to the channels that refused them, and prune old runs. Call it from an interval
|
|
3080
3221
|
* (start()), a cron hitting the mounted routes, or by hand. Concurrent
|
|
3081
3222
|
* calls share one check.
|
|
3082
3223
|
*/
|
|
@@ -3296,7 +3437,7 @@ ${id}`;
|
|
|
3296
3437
|
this.warnedDeferredStart = true;
|
|
3297
3438
|
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.');
|
|
3298
3439
|
}
|
|
3299
|
-
const tick = () => this.check().catch((e) => this.
|
|
3440
|
+
const tick = () => this.check().catch((e) => this.report(e, "check"));
|
|
3300
3441
|
this.timer = setInterval(tick, ms);
|
|
3301
3442
|
if (typeof this.timer === "object" && "unref" in this.timer) this.timer.unref();
|
|
3302
3443
|
this.firstTick = setTimeout(() => {
|
|
@@ -3345,21 +3486,28 @@ ${id}`;
|
|
|
3345
3486
|
if (alerts.length === 0 || this.deferDelivery) return alerts;
|
|
3346
3487
|
const delivered = [];
|
|
3347
3488
|
const failed = [];
|
|
3489
|
+
let reached = false;
|
|
3348
3490
|
for (const alert of alerts) {
|
|
3349
3491
|
if (this.triage && alert.type !== "recovered") await this.addTriage(alert, TRIAGE_TIMEOUT_MS);
|
|
3350
|
-
|
|
3492
|
+
const sent = await this.deliver(alert);
|
|
3493
|
+
reached ||= sent.reached;
|
|
3494
|
+
if (sent.left) failed.push(sent.left);
|
|
3495
|
+
else delivered.push(alert);
|
|
3351
3496
|
}
|
|
3352
|
-
await this.recordDelivery(name, delivered, failed, [], now);
|
|
3497
|
+
await this.recordDelivery(name, delivered, failed, [], now, reached);
|
|
3353
3498
|
return alerts;
|
|
3354
3499
|
}
|
|
3355
3500
|
/**
|
|
3356
|
-
* Send the alerts
|
|
3357
|
-
* first
|
|
3358
|
-
*
|
|
3359
|
-
*
|
|
3360
|
-
*
|
|
3361
|
-
*
|
|
3362
|
-
*
|
|
3501
|
+
* Send the alerts some channel refused last time, once each, oldest
|
|
3502
|
+
* first, to the channels that still owe them (HeldAlert.owed) and no
|
|
3503
|
+
* others, from the copy held in state, its run trimmed (heldRun). They
|
|
3504
|
+
* are claimed first (claimUndelivered): moved into `sending` with a lease,
|
|
3505
|
+
* in one conditional write, so when several processes check at once only
|
|
3506
|
+
* the one whose write lands sends them. An alert that no longer describes
|
|
3507
|
+
* the job (staleAlert) is dropped instead. Retries across a check share
|
|
3508
|
+
* RETRY_BUDGET_MS of wall-clock time; claimed alerts it has no time left
|
|
3509
|
+
* for go back to the queue for the next check. Returns the alerts some
|
|
3510
|
+
* channel took on this attempt.
|
|
3363
3511
|
*/
|
|
3364
3512
|
async retryUndelivered(name, state, now, budget) {
|
|
3365
3513
|
if ((state.undelivered ?? []).length === 0 || this.deferDelivery) return [];
|
|
@@ -3370,12 +3518,13 @@ ${id}`;
|
|
|
3370
3518
|
return { state: claim.state, result: claim.claimed };
|
|
3371
3519
|
}));
|
|
3372
3520
|
} catch (e) {
|
|
3373
|
-
this.
|
|
3521
|
+
this.report(e, `claiming undelivered alerts for ${name}`);
|
|
3374
3522
|
return [];
|
|
3375
3523
|
}
|
|
3376
3524
|
if (claimed.length === 0) return [];
|
|
3377
3525
|
const delivered = [];
|
|
3378
3526
|
const failed = [];
|
|
3527
|
+
const sent = [];
|
|
3379
3528
|
for (const alert of claimed) {
|
|
3380
3529
|
const left = RETRY_BUDGET_MS - budget.spentMs;
|
|
3381
3530
|
if (left <= 0) {
|
|
@@ -3384,45 +3533,61 @@ ${id}`;
|
|
|
3384
3533
|
}
|
|
3385
3534
|
const started = Date.now();
|
|
3386
3535
|
if (this.triage && alert.type !== "recovered" && alert.triage === void 0) await this.addTriage(alert, Math.min(TRIAGE_TIMEOUT_MS, left));
|
|
3387
|
-
|
|
3536
|
+
const outcome = await this.deliver(alert);
|
|
3537
|
+
if (outcome.reached) sent.push(withoutOwed(alert));
|
|
3538
|
+
if (outcome.left) failed.push(outcome.left);
|
|
3539
|
+
else delivered.push(alert);
|
|
3388
3540
|
budget.spentMs += Math.max(0, Date.now() - started);
|
|
3389
3541
|
}
|
|
3390
|
-
await this.recordDelivery(name, delivered, failed, [], now);
|
|
3391
|
-
return
|
|
3542
|
+
await this.recordDelivery(name, delivered, failed, [], now, sent.length > 0);
|
|
3543
|
+
return sent;
|
|
3392
3544
|
}
|
|
3393
3545
|
/**
|
|
3394
3546
|
* Mark delivered alerts done, drop stale ones, and keep failed ones for the
|
|
3395
3547
|
* next check, taking them all out of `sending` (recordSent). A failed alert
|
|
3396
|
-
* replaces its stored copy, so a triage made on this attempt
|
|
3397
|
-
*
|
|
3398
|
-
*
|
|
3548
|
+
* replaces its stored copy, so a triage made on this attempt and the
|
|
3549
|
+
* channels that still owe it are kept. lastAlertAt moves when `reached`,
|
|
3550
|
+
* some channel accepted an alert on this attempt. When this write fails,
|
|
3551
|
+
* alerts still in `sending` are retried once their lease runs out.
|
|
3399
3552
|
*/
|
|
3400
|
-
async recordDelivery(name, delivered, failed, dropped, now) {
|
|
3553
|
+
async recordDelivery(name, delivered, failed, dropped, now, reached) {
|
|
3401
3554
|
try {
|
|
3402
3555
|
const { result: trimmed } = await this.updateState(name, (previous) => {
|
|
3403
|
-
const { state, dropped: letGo } = recordSent(normalizeState(previous, name), delivered, failed, dropped, now);
|
|
3556
|
+
const { state, dropped: letGo } = recordSent(normalizeState(previous, name), delivered, failed, dropped, now, reached);
|
|
3404
3557
|
return { state, result: letGo };
|
|
3405
3558
|
});
|
|
3406
3559
|
this.reportDropped(name, trimmed);
|
|
3407
3560
|
} catch (e) {
|
|
3408
|
-
this.
|
|
3561
|
+
this.report(e, `recording alert delivery for ${name}`);
|
|
3409
3562
|
}
|
|
3410
3563
|
}
|
|
3411
|
-
/**
|
|
3564
|
+
/**
|
|
3565
|
+
* Send to every channel that still owes the alert (owedChannels: all of
|
|
3566
|
+
* them for an alert not sent before), at once, each failure reported under
|
|
3567
|
+
* its channel's name. `left` is the alert with `owed` cut to the channels
|
|
3568
|
+
* that refused it, or null when none did (or none is left to owe it);
|
|
3569
|
+
* `reached` is whether any channel accepted it. With no channels at all it
|
|
3570
|
+
* counts as delivered, as it always has.
|
|
3571
|
+
*/
|
|
3412
3572
|
async deliver(alert) {
|
|
3413
|
-
if (this.alerts.length === 0) return true;
|
|
3414
|
-
const
|
|
3415
|
-
|
|
3573
|
+
if (this.alerts.length === 0) return { left: null, reached: true };
|
|
3574
|
+
const keys = channelKeys(this.alerts.map((channel) => channel.name));
|
|
3575
|
+
const owed = owedChannels(alert, keys);
|
|
3576
|
+
if (owed.length === 0) return { left: null, reached: false };
|
|
3577
|
+
const plain = withoutOwed(alert);
|
|
3578
|
+
const failed = [];
|
|
3579
|
+
await Promise.all(
|
|
3580
|
+
this.alerts.map(async (channel, i) => {
|
|
3581
|
+
if (!owed.includes(keys[i])) return;
|
|
3416
3582
|
try {
|
|
3417
|
-
await withTimeout(channel.send(
|
|
3418
|
-
return true;
|
|
3583
|
+
await withTimeout(channel.send(plain, { onError: (e) => this.report(e, `alert channel ${channel.name}`) }), CHANNEL_TIMEOUT_MS);
|
|
3419
3584
|
} catch (e) {
|
|
3420
|
-
|
|
3421
|
-
|
|
3585
|
+
failed.push(keys[i]);
|
|
3586
|
+
this.report(e, `alert channel ${channel.name}`);
|
|
3422
3587
|
}
|
|
3423
3588
|
})
|
|
3424
3589
|
);
|
|
3425
|
-
return
|
|
3590
|
+
return { left: afterSend(alert, owed, failed), reached: failed.length < owed.length };
|
|
3426
3591
|
}
|
|
3427
3592
|
/** Sets alert.triage to the diagnosis, or to null when there is none, so it is tried once per alert. */
|
|
3428
3593
|
async addTriage(alert, timeout) {
|
|
@@ -3434,7 +3599,7 @@ ${id}`;
|
|
|
3434
3599
|
} catch (e) {
|
|
3435
3600
|
controller.abort();
|
|
3436
3601
|
alert.triage = null;
|
|
3437
|
-
this.
|
|
3602
|
+
this.report(e, `triage for ${alert.job}`);
|
|
3438
3603
|
}
|
|
3439
3604
|
}
|
|
3440
3605
|
};
|