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