@cronwatch/sdk 0.12.5 → 0.13.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.
Files changed (119) hide show
  1. package/dist/anthropic.d.cts +1 -1
  2. package/dist/anthropic.d.ts +1 -1
  3. package/dist/bugsnag.cjs +7 -0
  4. package/dist/bugsnag.cjs.map +1 -1
  5. package/dist/bugsnag.d.cts +1 -1
  6. package/dist/bugsnag.d.ts +1 -1
  7. package/dist/bugsnag.js +7 -0
  8. package/dist/bugsnag.js.map +1 -1
  9. package/dist/{client-CFJUFEOg.d.cts → client-DAJH7YiG.d.ts} +28 -7
  10. package/dist/{client-k5UVIH38.d.ts → client-z1LdR_m2.d.cts} +28 -7
  11. package/dist/d1.d.cts +1 -1
  12. package/dist/d1.d.ts +1 -1
  13. package/dist/datadog.cjs +7 -0
  14. package/dist/datadog.cjs.map +1 -1
  15. package/dist/datadog.d.cts +1 -1
  16. package/dist/datadog.d.ts +1 -1
  17. package/dist/datadog.js +7 -0
  18. package/dist/datadog.js.map +1 -1
  19. package/dist/discord.cjs +25 -1
  20. package/dist/discord.cjs.map +1 -1
  21. package/dist/discord.d.cts +1 -1
  22. package/dist/discord.d.ts +1 -1
  23. package/dist/discord.js +25 -1
  24. package/dist/discord.js.map +1 -1
  25. package/dist/{email-U0hYRnPa.d.cts → email-CCUzlLe9.d.cts} +1 -1
  26. package/dist/{email-CPoxD8--.d.ts → email-CFtDqlvZ.d.ts} +1 -1
  27. package/dist/honeybadger.cjs +7 -0
  28. package/dist/honeybadger.cjs.map +1 -1
  29. package/dist/honeybadger.d.cts +1 -1
  30. package/dist/honeybadger.d.ts +1 -1
  31. package/dist/honeybadger.js +7 -0
  32. package/dist/honeybadger.js.map +1 -1
  33. package/dist/index.cjs +191 -47
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +5 -5
  36. package/dist/index.d.ts +5 -5
  37. package/dist/index.js +191 -47
  38. package/dist/index.js.map +1 -1
  39. package/dist/mailgun.cjs +7 -0
  40. package/dist/mailgun.cjs.map +1 -1
  41. package/dist/mailgun.d.cts +2 -2
  42. package/dist/mailgun.d.ts +2 -2
  43. package/dist/mailgun.js +7 -0
  44. package/dist/mailgun.js.map +1 -1
  45. package/dist/newrelic.cjs +7 -0
  46. package/dist/newrelic.cjs.map +1 -1
  47. package/dist/newrelic.d.cts +1 -1
  48. package/dist/newrelic.d.ts +1 -1
  49. package/dist/newrelic.js +7 -0
  50. package/dist/newrelic.js.map +1 -1
  51. package/dist/pg-cron.cjs.map +1 -1
  52. package/dist/pg-cron.d.cts +2 -2
  53. package/dist/pg-cron.d.ts +2 -2
  54. package/dist/pg-cron.js.map +1 -1
  55. package/dist/postgres.cjs +7 -2
  56. package/dist/postgres.cjs.map +1 -1
  57. package/dist/postgres.d.cts +1 -1
  58. package/dist/postgres.d.ts +1 -1
  59. package/dist/postgres.js +7 -2
  60. package/dist/postgres.js.map +1 -1
  61. package/dist/postmark.cjs +7 -0
  62. package/dist/postmark.cjs.map +1 -1
  63. package/dist/postmark.d.cts +2 -2
  64. package/dist/postmark.d.ts +2 -2
  65. package/dist/postmark.js +7 -0
  66. package/dist/postmark.js.map +1 -1
  67. package/dist/resend.cjs +7 -0
  68. package/dist/resend.cjs.map +1 -1
  69. package/dist/resend.d.cts +2 -2
  70. package/dist/resend.d.ts +2 -2
  71. package/dist/resend.js +7 -0
  72. package/dist/resend.js.map +1 -1
  73. package/dist/rollbar.cjs +7 -0
  74. package/dist/rollbar.cjs.map +1 -1
  75. package/dist/rollbar.d.cts +1 -1
  76. package/dist/rollbar.d.ts +1 -1
  77. package/dist/rollbar.js +7 -0
  78. package/dist/rollbar.js.map +1 -1
  79. package/dist/sendgrid.cjs +7 -0
  80. package/dist/sendgrid.cjs.map +1 -1
  81. package/dist/sendgrid.d.cts +2 -2
  82. package/dist/sendgrid.d.ts +2 -2
  83. package/dist/sendgrid.js +7 -0
  84. package/dist/sendgrid.js.map +1 -1
  85. package/dist/sentry.cjs +7 -0
  86. package/dist/sentry.cjs.map +1 -1
  87. package/dist/sentry.d.cts +1 -1
  88. package/dist/sentry.d.ts +1 -1
  89. package/dist/sentry.js +7 -0
  90. package/dist/sentry.js.map +1 -1
  91. package/dist/ses.cjs +7 -0
  92. package/dist/ses.cjs.map +1 -1
  93. package/dist/ses.d.cts +2 -2
  94. package/dist/ses.d.ts +2 -2
  95. package/dist/ses.js +7 -0
  96. package/dist/ses.js.map +1 -1
  97. package/dist/slack.cjs +32 -1
  98. package/dist/slack.cjs.map +1 -1
  99. package/dist/slack.d.cts +1 -1
  100. package/dist/slack.d.ts +1 -1
  101. package/dist/slack.js +32 -1
  102. package/dist/slack.js.map +1 -1
  103. package/dist/sqlite.d.cts +1 -1
  104. package/dist/sqlite.d.ts +1 -1
  105. package/dist/twilio.cjs +7 -0
  106. package/dist/twilio.cjs.map +1 -1
  107. package/dist/twilio.d.cts +1 -1
  108. package/dist/twilio.d.ts +1 -1
  109. package/dist/twilio.js +7 -0
  110. package/dist/twilio.js.map +1 -1
  111. package/dist/{types-V8SN54o9.d.cts → types-D6t0Z846.d.cts} +8 -0
  112. package/dist/{types-V8SN54o9.d.ts → types-D6t0Z846.d.ts} +8 -0
  113. package/dist/webhook.cjs +11 -1
  114. package/dist/webhook.cjs.map +1 -1
  115. package/dist/webhook.d.cts +3 -2
  116. package/dist/webhook.d.ts +3 -2
  117. package/dist/webhook.js +11 -1
  118. package/dist/webhook.js.map +1 -1
  119. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as CronwatchOptions, a as Cronwatch } from './client-CFJUFEOg.cjs';
2
- export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-CFJUFEOg.cjs';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-V8SN54o9.cjs';
4
- export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-V8SN54o9.cjs';
1
+ import { C as CronwatchOptions, a as Cronwatch } from './client-z1LdR_m2.cjs';
2
+ export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-z1LdR_m2.cjs';
3
+ import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-D6t0Z846.cjs';
4
+ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-D6t0Z846.cjs';
5
5
 
6
6
  /**
7
7
  * Keeps everything in process memory. The default when no store is given,
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
11
11
  declare function memory(): Store;
12
12
 
13
13
  /** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
14
- declare const VERSION = "0.12.5";
14
+ declare const VERSION = "0.13.0";
15
15
 
16
16
  /**
17
17
  * "15m" -> 900000. Accepts a plain number of milliseconds, and compound
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as CronwatchOptions, a as Cronwatch } from './client-k5UVIH38.js';
2
- export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-k5UVIH38.js';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-V8SN54o9.js';
4
- export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-V8SN54o9.js';
1
+ import { C as CronwatchOptions, a as Cronwatch } from './client-DAJH7YiG.js';
2
+ export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-DAJH7YiG.js';
3
+ import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-D6t0Z846.js';
4
+ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-D6t0Z846.js';
5
5
 
6
6
  /**
7
7
  * Keeps everything in process memory. The default when no store is given,
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
11
11
  declare function memory(): Store;
12
12
 
13
13
  /** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
14
- declare const VERSION = "0.12.5";
14
+ declare const VERSION = "0.13.0";
15
15
 
16
16
  /**
17
17
  * "15m" -> 900000. Accepts a plain number of milliseconds, and compound
package/dist/index.js CHANGED
@@ -79,27 +79,31 @@ function readEnv(name) {
79
79
  return typeof process === "undefined" || !process.env ? void 0 : process.env[name];
80
80
  }
81
81
  function readSecretEnv(name) {
82
- const value = readEnv(name);
83
- return value === void 0 || value.trim() === "" ? void 0 : value;
82
+ const value = readEnv(name)?.trim();
83
+ return value === void 0 || value === "" ? void 0 : value;
84
84
  }
85
85
  function secretOption(value, what) {
86
86
  if (value === void 0 || value === null) return value;
87
87
  if (typeof value !== "string") {
88
88
  throw new TypeError(`${what} must be a string, or null to opt out, not ${Array.isArray(value) ? "an array" : typeof value}`);
89
89
  }
90
- return value.trim() === "" ? void 0 : value;
90
+ const trimmed = value.trim();
91
+ return trimmed === "" ? void 0 : trimmed;
91
92
  }
92
- function environment() {
93
- if (typeof process === "undefined" || !process.env) return "";
94
- for (const value of [process.env.CRONWATCH_ENV, process.env.APP_ENV, process.env.NODE_ENV]) {
93
+ function environmentOf(values) {
94
+ for (const value of values) {
95
95
  const name = typeof value === "string" ? value.trim().toLowerCase() : "";
96
96
  if (name === "") continue;
97
97
  if (name === "prod") return "production";
98
- if (name === "dev" || name === "local" || name === "test" || name === "testing") return "development";
98
+ if (name === "dev" || name === "local") return "development";
99
99
  return name;
100
100
  }
101
101
  return "";
102
102
  }
103
+ function environment() {
104
+ if (typeof process === "undefined" || !process.env) return "";
105
+ return environmentOf([process.env.CRONWATCH_ENV, process.env.APP_ENV, process.env.NODE_ENV]);
106
+ }
103
107
  function isDevelopment() {
104
108
  return environment() === "development";
105
109
  }
@@ -326,7 +330,7 @@ function isObject(value) {
326
330
  }
327
331
  function normalizeState(state, job) {
328
332
  if (!isObject(state)) return emptyState(job);
329
- const { sending, underFloor, ...rest } = state;
333
+ const { sending, underFloor, pendingRuns, ...rest } = state;
330
334
  const open = isObject(state.open) ? Object.fromEntries(Object.entries(state.open).filter(([, at]) => typeof at === "number")) : {};
331
335
  const number = (value) => typeof value === "number" ? value : null;
332
336
  const list = (value) => Array.isArray(value) ? value : [];
@@ -340,7 +344,8 @@ function normalizeState(state, job) {
340
344
  pendingRecovery: list(state.pendingRecovery).filter((c) => typeof c === "string"),
341
345
  undelivered: list(state.undelivered).filter((alert) => isObject(alert)),
342
346
  ...Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {},
343
- ...Array.isArray(underFloor) && underFloor.some((m) => typeof m === "string") ? { underFloor: underFloor.filter((m) => typeof m === "string") } : {}
347
+ ...Array.isArray(underFloor) && underFloor.some((m) => typeof m === "string") ? { underFloor: underFloor.filter((m) => typeof m === "string") } : {},
348
+ ...Array.isArray(pendingRuns) && pendingRuns.some((id) => typeof id === "string") ? { pendingRuns: pendingRuns.filter((id) => typeof id === "string") } : {}
344
349
  };
345
350
  }
346
351
  var MAX_UNDELIVERED = 20;
@@ -390,6 +395,34 @@ function recordSent(state, delivered, failed, stale, now) {
390
395
  if (delivered.length > 0) next.lastAlertAt = now;
391
396
  return queueUndelivered(next, failed);
392
397
  }
398
+ function claimUndelivered(state, now, until) {
399
+ const next = cloneState(state);
400
+ const queue = next.undelivered;
401
+ const stale = queue.filter((alert) => staleAlert(alert, next));
402
+ const silenced = isSilenced(next, now);
403
+ const room = Math.max(0, MAX_UNDELIVERED - (next.sending ?? []).length);
404
+ const claimed = queue.filter((alert) => !stale.includes(alert) && (!silenced || alert.type === "recovered")).slice(0, room);
405
+ if (stale.length === 0 && claimed.length === 0) return { state, claimed, stale };
406
+ next.undelivered = queue.filter((alert) => !stale.includes(alert) && !claimed.includes(alert));
407
+ if (claimed.length > 0) next.sending = [...next.sending ?? [], ...claimed.map((alert) => ({ until, alert }))];
408
+ return { state: next, claimed, stale };
409
+ }
410
+ function holdRun(state, runId) {
411
+ const next = cloneState(state);
412
+ const held = (next.pendingRuns ?? []).filter((id) => id !== runId);
413
+ held.push(runId);
414
+ next.pendingRuns = held.slice(-100);
415
+ return next;
416
+ }
417
+ function releaseRun(state, runId) {
418
+ const held = state.pendingRuns ?? [];
419
+ if (!held.includes(runId)) return state;
420
+ const next = cloneState(state);
421
+ const left = held.filter((id) => id !== runId);
422
+ if (left.length > 0) next.pendingRuns = left;
423
+ else delete next.pendingRuns;
424
+ return next;
425
+ }
393
426
  function cloneState(state) {
394
427
  return normalizeState(state, state.job);
395
428
  }
@@ -570,7 +603,7 @@ function isSilenced(state, now) {
570
603
  }
571
604
  function applySilence(previous, evaluation, now) {
572
605
  if (!isSilenced(previous, now)) return evaluation;
573
- return { state: muteOpens(previous, evaluation.state), alerts: [] };
606
+ return { state: muteOpens(previous, evaluation.state), alerts: evaluation.alerts.filter((alert) => alert.type === "recovered") };
574
607
  }
575
608
  function staleAlert(alert, state) {
576
609
  if (alert.type === "recovered") {
@@ -727,10 +760,10 @@ function json(body, status = 200, headers = {}) {
727
760
  headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...headers }
728
761
  });
729
762
  }
730
- function constantTimeEqual(a, b) {
731
- if (a.length !== b.length) return false;
732
- let diff = 0;
733
- for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
763
+ function constantTimeEqual(sent, secret) {
764
+ if (secret.length === 0) return sent.length === 0;
765
+ let diff = sent.length ^ secret.length;
766
+ for (let i = 0; i < sent.length; i++) diff |= sent.charCodeAt(i) ^ secret.charCodeAt(i % secret.length);
734
767
  return diff === 0;
735
768
  }
736
769
 
@@ -1043,7 +1076,7 @@ function staticAsset(path, base) {
1043
1076
  }
1044
1077
 
1045
1078
  // src/version.ts
1046
- var VERSION = "0.12.5";
1079
+ var VERSION = "0.13.0";
1047
1080
  var API_VERSION = 1;
1048
1081
 
1049
1082
  // src/routes/timeline.ts
@@ -1830,23 +1863,63 @@ function stripBase(pathname, base) {
1830
1863
  if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
1831
1864
  return path;
1832
1865
  }
1866
+ var MAX_REQUEST_BODY = 1 << 20;
1867
+ var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
1868
+ async function readLimited(request) {
1869
+ const length = Number(request.headers.get("content-length"));
1870
+ if (Number.isFinite(length) && length > MAX_REQUEST_BODY) return TOO_LARGE;
1871
+ if (!request.body) return new Uint8Array(new ArrayBuffer(0));
1872
+ const reader = request.body.getReader();
1873
+ const chunks = [];
1874
+ let size = 0;
1875
+ try {
1876
+ for (; ; ) {
1877
+ const { done, value } = await reader.read();
1878
+ if (done) break;
1879
+ size += value.byteLength;
1880
+ if (size > MAX_REQUEST_BODY) {
1881
+ await reader.cancel().catch(() => {
1882
+ });
1883
+ return TOO_LARGE;
1884
+ }
1885
+ chunks.push(value);
1886
+ }
1887
+ } finally {
1888
+ reader.releaseLock();
1889
+ }
1890
+ const out = new Uint8Array(new ArrayBuffer(size));
1891
+ let at = 0;
1892
+ for (const chunk of chunks) {
1893
+ out.set(chunk, at);
1894
+ at += chunk.byteLength;
1895
+ }
1896
+ return out;
1897
+ }
1833
1898
  async function readBody(request) {
1834
1899
  const type = request.headers.get("content-type") ?? "";
1900
+ const json2 = type.includes("application/json");
1901
+ const form = type.includes("application/x-www-form-urlencoded") || type.includes("multipart/form-data");
1902
+ if (!json2 && !form) return {};
1903
+ let bytes;
1904
+ try {
1905
+ bytes = await readLimited(request);
1906
+ } catch {
1907
+ return {};
1908
+ }
1909
+ if (bytes === TOO_LARGE) return TOO_LARGE;
1910
+ const copy = new Request(request.url, { method: "POST", headers: { "content-type": type }, body: bytes });
1835
1911
  try {
1836
- if (type.includes("application/json")) {
1837
- const data = await request.json();
1912
+ if (json2) {
1913
+ const data = await copy.json();
1838
1914
  return typeof data === "object" && data !== null ? Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v)])) : {};
1839
1915
  }
1840
- if (type.includes("application/x-www-form-urlencoded") || type.includes("multipart/form-data")) {
1841
- const form = await request.formData();
1842
- const out = {};
1843
- for (const [k, v] of form.entries()) out[k] = String(v);
1844
- return out;
1845
- }
1916
+ const fields = await copy.formData();
1917
+ const out = {};
1918
+ for (const [k, v] of fields.entries()) out[k] = String(v);
1919
+ return out;
1846
1920
  } catch {
1847
1921
  return {};
1848
1922
  }
1849
- return {};
1850
1923
  }
1851
1924
  function crossSite(request, publicOrigin) {
1852
1925
  const origin = request.headers.get("origin");
@@ -1910,7 +1983,8 @@ function buildRoutes(cw, options = {}) {
1910
1983
  };
1911
1984
  const signInPage = () => generated ? html(messagePage("Sign in", "CRONWATCH_TOKEN is not set, so this development server made a token. The sign-in link is in the server log: open it once, or enter the token from it below, and this browser stays signed in.", base, true), 401) : html(messagePage("Sign in", "Enter your CRONWATCH_TOKEN and this browser stays signed in.", base, true), 401);
1912
1985
  if (token && method === "POST" && path === "/signin") {
1913
- const sent = (await readBody(request)).token;
1986
+ const body = await readBody(request);
1987
+ const sent = body === TOO_LARGE ? void 0 : body.token;
1914
1988
  if (sent === void 0 || !constantTimeEqual(sent, token)) return signInPage();
1915
1989
  return redirect(signInReturn(request.headers.get("referer"), publicOrigin, base), { "set-cookie": await signInCookie() });
1916
1990
  }
@@ -1926,7 +2000,7 @@ function buildRoutes(cw, options = {}) {
1926
2000
  }
1927
2001
  if (query !== null) {
1928
2002
  url.searchParams.delete("token");
1929
- return redirect(url.pathname + (url.search || ""), { "set-cookie": await signInCookie() });
2003
+ return redirect(url.pathname.replace(/^\/+/, "/") + (url.search || ""), { "set-cookie": await signInCookie() });
1930
2004
  }
1931
2005
  }
1932
2006
  const redirectBack = () => {
@@ -1965,9 +2039,11 @@ function buildRoutes(cw, options = {}) {
1965
2039
  if (parts[2] !== "silence" && parts[2] !== "unsilence") return html(messagePage("Not found", path, base), 404);
1966
2040
  if (!await cw.jobSummary(name)) return html(messagePage("No such job", `${name} is not in the store.`, base), 404);
1967
2041
  if (parts[2] === "silence") {
2042
+ const body = await readBody(request);
2043
+ if (body === TOO_LARGE) return html(messagePage("Not silenced", "The request was too large.", base), 413);
1968
2044
  let duration;
1969
2045
  try {
1970
- duration = silenceDuration((await readBody(request)).for);
2046
+ duration = silenceDuration(body.for);
1971
2047
  } catch (e) {
1972
2048
  return html(messagePage("Not silenced", e.message, base), 400);
1973
2049
  }
@@ -2001,6 +2077,7 @@ function buildRoutes(cw, options = {}) {
2001
2077
  if (!await cw.jobSummary(name)) return api({ ok: false, error: "No such job" }, 404);
2002
2078
  if (rest[2] === "silence") {
2003
2079
  const body = await readBody(request);
2080
+ if (body === TOO_LARGE) return api({ ok: false, error: "Request body too large" }, 413);
2004
2081
  let duration;
2005
2082
  try {
2006
2083
  duration = silenceDuration(body.for ?? url.searchParams.get("for"));
@@ -2250,6 +2327,11 @@ var Cronwatch = class {
2250
2327
  deferDelivery;
2251
2328
  defaults;
2252
2329
  definitions = /* @__PURE__ */ new Map();
2330
+ /**
2331
+ * Declarations forgotten here (forget()), kept so that run() or resumeRun()
2332
+ * by name declares the job again as it was written, not bare.
2333
+ */
2334
+ forgotten = /* @__PURE__ */ new Map();
2253
2335
  synced = /* @__PURE__ */ new Set();
2254
2336
  /** The tail of each job's queue of state updates. See serial(). */
2255
2337
  queues = /* @__PURE__ */ new Map();
@@ -2304,13 +2386,26 @@ var Cronwatch = class {
2304
2386
  const definition = { ...this.defaults, ...options, name };
2305
2387
  validateDefinition(definition);
2306
2388
  this.definitions.set(name, definition);
2389
+ this.forgotten.delete(name);
2307
2390
  this.synced.delete(name);
2308
2391
  return this.handle(definition);
2309
2392
  }
2393
+ /** The declaration of `name`: the standing one, else one forgotten here, declared again as it was. */
2394
+ declared(name) {
2395
+ const standing = this.definitions.get(name);
2396
+ if (standing) return standing;
2397
+ const forgotten = this.forgotten.get(name);
2398
+ if (!forgotten) return void 0;
2399
+ this.forgotten.delete(name);
2400
+ this.definitions.set(name, forgotten);
2401
+ this.synced.delete(name);
2402
+ return forgotten;
2403
+ }
2310
2404
  run(name, optionsOrFn, maybeFn) {
2311
2405
  const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
2312
2406
  const options = typeof optionsOrFn === "function" ? void 0 : optionsOrFn;
2313
- const handle = options || !this.definitions.has(name) ? this.job(name, options) : this.handle(this.definitions.get(name));
2407
+ const declared = options ? void 0 : this.declared(name);
2408
+ const handle = declared ? this.handle(declared) : this.job(name, options);
2314
2409
  return handle.run(fn);
2315
2410
  }
2316
2411
  /** The definitions declared in this process. */
@@ -2319,7 +2414,7 @@ var Cronwatch = class {
2319
2414
  }
2320
2415
  /** A handle on a run started elsewhere, as job(name).resume(runId). The job must be declared in this process. */
2321
2416
  resumeRun(name, runId) {
2322
- const definition = this.definitions.get(name);
2417
+ const definition = this.declared(name);
2323
2418
  if (!definition) return Promise.reject(new Error(`resumeRun: job "${name}" is not declared; call job() first`));
2324
2419
  return this.resumeHandle(definition, runId);
2325
2420
  }
@@ -2535,7 +2630,7 @@ var Cronwatch = class {
2535
2630
  } catch (e) {
2536
2631
  this.onError(e, `recording ${name}`);
2537
2632
  }
2538
- const started = recorded ? this.updateState(name, (before) => ({ state: onRunStart(before), result: void 0 })).then(() => {
2633
+ const started = recorded ? this.updateState(name, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 })).then(() => {
2539
2634
  }, (e) => this.onError(e, `starting ${name}`)) : Promise.resolve();
2540
2635
  const recorder = createRecorder(run);
2541
2636
  const clearTimer = after(timeoutMs(definition), () => recorder.abort());
@@ -2691,7 +2786,7 @@ ${id}`;
2691
2786
  this.onError(e, `recording ${name}`);
2692
2787
  }
2693
2788
  if (recorded) {
2694
- await this.updateState(name, (before) => ({ state: onRunStart(before), result: void 0 })).catch((e) => this.onError(e, `starting ${name}`));
2789
+ await this.updateState(name, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 })).catch((e) => this.onError(e, `starting ${name}`));
2695
2790
  }
2696
2791
  return this.runHandle(definition, run.id, run, recorded, null);
2697
2792
  }
@@ -2904,7 +2999,7 @@ ${id}`;
2904
2999
  throw e;
2905
3000
  }
2906
3001
  if (!evaluate) return [];
2907
- await this.updateState(run.job, (before) => ({ state: onRunStart(before), result: void 0 }));
3002
+ await this.updateState(run.job, (before) => ({ state: holdRun(onRunStart(before), run.id), result: void 0 }));
2908
3003
  if (run.status === "running") return [];
2909
3004
  return this.finishRun(definition, run, this.now());
2910
3005
  }
@@ -2920,7 +3015,11 @@ ${id}`;
2920
3015
  this.report(new Error(`run ${run.id} of ${run.job} ${claim.ignored}; ignored`), `recording ${run.job}`);
2921
3016
  return [];
2922
3017
  }
2923
- if (!evaluate || claim.lateAfterTimeout && run.status !== "ok") return [];
3018
+ if (!evaluate) {
3019
+ await this.updateState(run.job, (current) => ({ state: releaseRun(current, run.id), result: void 0 })).catch((e) => this.onError(e, `recording ${run.job}`));
3020
+ return [];
3021
+ }
3022
+ if (claim.lateAfterTimeout && run.status !== "ok") return [];
2924
3023
  return this.finishRun(definition, run, this.now());
2925
3024
  }
2926
3025
  /**
@@ -2934,7 +3033,8 @@ ${id}`;
2934
3033
  let history = null;
2935
3034
  ({ result: held } = await this.updateState(run.job, async (previous) => {
2936
3035
  history ??= await this.history(run);
2937
- return this.outbox(applySilence(previous, onRunFinish(definition, run, previous, history, now), now), definition, now);
3036
+ const before = releaseRun(previous, run.id);
3037
+ return this.outbox(applySilence(before, onRunFinish(definition, run, before, history, now), now), definition, now);
2938
3038
  }));
2939
3039
  } catch (e) {
2940
3040
  this.onError(e, `evaluating ${run.job}`);
@@ -3039,6 +3139,7 @@ ${id}`;
3039
3139
  this.reportDropped(stored.name, held.dropped);
3040
3140
  alerts.push(...await this.retryUndelivered(stored.name, state, now, retries));
3041
3141
  alerts.push(...await this.dispatch(stored.name, held.alerts, now));
3142
+ alerts.push(...await this.evaluatePending(stored, recent, state, now));
3042
3143
  jobs.push(summarize(stored, recent, state, nextExpectedAt, now));
3043
3144
  } catch (e) {
3044
3145
  this.onError(e, `checking ${stored.name}`);
@@ -3056,6 +3157,32 @@ ${id}`;
3056
3157
  }
3057
3158
  return { checkedAt: now, jobs, alerts, pruned };
3058
3159
  }
3160
+ /**
3161
+ * Runs the job's state still holds as pending (holdRun) whose finish was
3162
+ * written but never evaluated: the process that finished them stopped, or
3163
+ * its state write failed, between the two writes. Each one finished more
3164
+ * than SEND_LEASE_MS ago is evaluated now, as its finish would have been;
3165
+ * one still running is left to the stuck check, and one the store no
3166
+ * longer has (given back, or pruned) is let go. The wait leaves a finish
3167
+ * under way in another process to write its own evaluation.
3168
+ */
3169
+ async evaluatePending(stored, recent, state, now) {
3170
+ const alerts = [];
3171
+ for (const id of state.pendingRuns ?? []) {
3172
+ try {
3173
+ const run = recent.find((r) => r.id === id) ?? await this.store.getRun(id);
3174
+ if (!run || run.job !== stored.name) {
3175
+ await this.updateState(stored.name, (current) => ({ state: releaseRun(current, id), result: void 0 }));
3176
+ continue;
3177
+ }
3178
+ if (run.status === "running" || typeof run.finishedAt !== "number" || run.finishedAt > now - SEND_LEASE_MS) continue;
3179
+ alerts.push(...await this.finishRun(stored.definition, run, now));
3180
+ } catch (e) {
3181
+ this.onError(e, `evaluating run ${id} of ${stored.name}`);
3182
+ }
3183
+ }
3184
+ return alerts;
3185
+ }
3059
3186
  /** A job's summary and its newest runs, without alerting. A job that cannot be evaluated is reported and shown as failing. */
3060
3187
  async snapshot(stored, now, runs) {
3061
3188
  let recent = [];
@@ -3134,11 +3261,15 @@ ${id}`;
3134
3261
  }
3135
3262
  /**
3136
3263
  * Remove a job and its runs from the store. A job still declared in code
3137
- * comes back: here on its next run, and in any other process that declares
3138
- * it on its next run there, or at that process's next check or dashboard read.
3264
+ * comes back, with no history: here on its next run, declared as it was
3265
+ * written (cw.run() and cw.resumeRun() by name included), and in any other
3266
+ * process that declares it on its next run there, or at that process's
3267
+ * next check or dashboard read.
3139
3268
  */
3140
3269
  async forget(name) {
3141
3270
  await this.ensureReady();
3271
+ const definition = this.definitions.get(name);
3272
+ if (definition) this.forgotten.set(name, definition);
3142
3273
  this.definitions.delete(name);
3143
3274
  this.synced.delete(name);
3144
3275
  await this.store.deleteJob(name);
@@ -3223,27 +3354,40 @@ ${id}`;
3223
3354
  }
3224
3355
  /**
3225
3356
  * Send the alerts that no channel accepted last time, once each, oldest
3226
- * first. `state` is the job's state as this check left it: an alert that
3227
- * no longer describes it (staleAlert) is dropped instead. Retries across a
3228
- * check share RETRY_BUDGET_MS of wall-clock time; once it is spent the rest
3229
- * stay queued for the next check.
3357
+ * first. They are claimed first (claimUndelivered): moved into `sending`
3358
+ * with a lease, in one conditional write, so when several processes check
3359
+ * at once only the one whose write lands sends them. An alert that no
3360
+ * longer describes the job (staleAlert) is dropped instead. Retries across
3361
+ * a check share RETRY_BUDGET_MS of wall-clock time; claimed alerts it has
3362
+ * no time left for go back to the queue for the next check.
3230
3363
  */
3231
3364
  async retryUndelivered(name, state, now, budget) {
3232
- const pending = state.undelivered ?? [];
3233
- if (pending.length === 0 || isSilenced(state, now) || this.deferDelivery) return [];
3365
+ if ((state.undelivered ?? []).length === 0 || this.deferDelivery) return [];
3366
+ let claimed;
3367
+ try {
3368
+ ({ result: claimed } = await this.updateState(name, (previous) => {
3369
+ const claim = claimUndelivered(previous, now, this.now() + SEND_LEASE_MS);
3370
+ return { state: claim.state, result: claim.claimed };
3371
+ }));
3372
+ } catch (e) {
3373
+ this.onError(e, `claiming undelivered alerts for ${name}`);
3374
+ return [];
3375
+ }
3376
+ if (claimed.length === 0) return [];
3234
3377
  const delivered = [];
3235
3378
  const failed = [];
3236
- const dropped = pending.filter((alert) => staleAlert(alert, state));
3237
- for (const alert of pending) {
3238
- if (dropped.includes(alert)) continue;
3379
+ for (const alert of claimed) {
3239
3380
  const left = RETRY_BUDGET_MS - budget.spentMs;
3240
- if (left <= 0) break;
3381
+ if (left <= 0) {
3382
+ failed.push(alert);
3383
+ continue;
3384
+ }
3241
3385
  const started = Date.now();
3242
3386
  if (this.triage && alert.type !== "recovered" && alert.triage === void 0) await this.addTriage(alert, Math.min(TRIAGE_TIMEOUT_MS, left));
3243
3387
  (await this.deliver(alert) ? delivered : failed).push(alert);
3244
3388
  budget.spentMs += Math.max(0, Date.now() - started);
3245
3389
  }
3246
- await this.recordDelivery(name, delivered, failed, dropped, now);
3390
+ await this.recordDelivery(name, delivered, failed, [], now);
3247
3391
  return delivered;
3248
3392
  }
3249
3393
  /**