@cronwatch/sdk 0.7.0 → 0.9.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 (111) hide show
  1. package/dist/anthropic.cjs +13 -2
  2. package/dist/anthropic.cjs.map +1 -1
  3. package/dist/anthropic.d.cts +1 -1
  4. package/dist/anthropic.d.ts +1 -1
  5. package/dist/anthropic.js +13 -2
  6. package/dist/anthropic.js.map +1 -1
  7. package/dist/bugsnag.cjs +8 -1
  8. package/dist/bugsnag.cjs.map +1 -1
  9. package/dist/bugsnag.d.cts +1 -1
  10. package/dist/bugsnag.d.ts +1 -1
  11. package/dist/bugsnag.js +8 -1
  12. package/dist/bugsnag.js.map +1 -1
  13. package/dist/{client-B5hM4l-D.d.ts → client-CQm-8-mR.d.ts} +13 -2
  14. package/dist/{client-Di5HAWQz.d.cts → client-D35cfnNV.d.cts} +13 -2
  15. package/dist/d1.cjs +33 -20
  16. package/dist/d1.cjs.map +1 -1
  17. package/dist/d1.d.cts +1 -1
  18. package/dist/d1.d.ts +1 -1
  19. package/dist/d1.js +33 -20
  20. package/dist/d1.js.map +1 -1
  21. package/dist/datadog.cjs.map +1 -1
  22. package/dist/datadog.d.cts +1 -1
  23. package/dist/datadog.d.ts +1 -1
  24. package/dist/datadog.js.map +1 -1
  25. package/dist/discord.cjs.map +1 -1
  26. package/dist/discord.d.cts +1 -1
  27. package/dist/discord.d.ts +1 -1
  28. package/dist/discord.js.map +1 -1
  29. package/dist/{email-Ciq7GRn4.d.ts → email-BxCp9RQV.d.ts} +1 -1
  30. package/dist/{email-DX6gkd5H.d.cts → email-D1d5niMy.d.cts} +1 -1
  31. package/dist/honeybadger.cjs +8 -1
  32. package/dist/honeybadger.cjs.map +1 -1
  33. package/dist/honeybadger.d.cts +1 -1
  34. package/dist/honeybadger.d.ts +1 -1
  35. package/dist/honeybadger.js +8 -1
  36. package/dist/honeybadger.js.map +1 -1
  37. package/dist/index.cjs +119 -35
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +4 -4
  40. package/dist/index.d.ts +4 -4
  41. package/dist/index.js +119 -35
  42. package/dist/index.js.map +1 -1
  43. package/dist/mailgun.cjs.map +1 -1
  44. package/dist/mailgun.d.cts +2 -2
  45. package/dist/mailgun.d.ts +2 -2
  46. package/dist/mailgun.js.map +1 -1
  47. package/dist/newrelic.cjs.map +1 -1
  48. package/dist/newrelic.d.cts +1 -1
  49. package/dist/newrelic.d.ts +1 -1
  50. package/dist/newrelic.js.map +1 -1
  51. package/dist/pg-cron.cjs +53 -5
  52. package/dist/pg-cron.cjs.map +1 -1
  53. package/dist/pg-cron.d.cts +5 -2
  54. package/dist/pg-cron.d.ts +5 -2
  55. package/dist/pg-cron.js +53 -5
  56. package/dist/pg-cron.js.map +1 -1
  57. package/dist/postgres.cjs +28 -15
  58. package/dist/postgres.cjs.map +1 -1
  59. package/dist/postgres.d.cts +1 -1
  60. package/dist/postgres.d.ts +1 -1
  61. package/dist/postgres.js +28 -15
  62. package/dist/postgres.js.map +1 -1
  63. package/dist/postmark.cjs.map +1 -1
  64. package/dist/postmark.d.cts +2 -2
  65. package/dist/postmark.d.ts +2 -2
  66. package/dist/postmark.js.map +1 -1
  67. package/dist/resend.cjs.map +1 -1
  68. package/dist/resend.d.cts +2 -2
  69. package/dist/resend.d.ts +2 -2
  70. package/dist/resend.js.map +1 -1
  71. package/dist/rollbar.cjs +8 -1
  72. package/dist/rollbar.cjs.map +1 -1
  73. package/dist/rollbar.d.cts +1 -1
  74. package/dist/rollbar.d.ts +1 -1
  75. package/dist/rollbar.js +8 -1
  76. package/dist/rollbar.js.map +1 -1
  77. package/dist/sendgrid.cjs.map +1 -1
  78. package/dist/sendgrid.d.cts +2 -2
  79. package/dist/sendgrid.d.ts +2 -2
  80. package/dist/sendgrid.js.map +1 -1
  81. package/dist/sentry.cjs +8 -1
  82. package/dist/sentry.cjs.map +1 -1
  83. package/dist/sentry.d.cts +1 -1
  84. package/dist/sentry.d.ts +1 -1
  85. package/dist/sentry.js +8 -1
  86. package/dist/sentry.js.map +1 -1
  87. package/dist/ses.cjs.map +1 -1
  88. package/dist/ses.d.cts +2 -2
  89. package/dist/ses.d.ts +2 -2
  90. package/dist/ses.js.map +1 -1
  91. package/dist/slack.cjs.map +1 -1
  92. package/dist/slack.d.cts +1 -1
  93. package/dist/slack.d.ts +1 -1
  94. package/dist/slack.js.map +1 -1
  95. package/dist/sqlite.cjs +30 -19
  96. package/dist/sqlite.cjs.map +1 -1
  97. package/dist/sqlite.d.cts +1 -1
  98. package/dist/sqlite.d.ts +1 -1
  99. package/dist/sqlite.js +30 -19
  100. package/dist/sqlite.js.map +1 -1
  101. package/dist/twilio.cjs.map +1 -1
  102. package/dist/twilio.d.cts +1 -1
  103. package/dist/twilio.d.ts +1 -1
  104. package/dist/twilio.js.map +1 -1
  105. package/dist/{types-BjBbCGeJ.d.cts → types-Cz47Boks.d.cts} +6 -0
  106. package/dist/{types-BjBbCGeJ.d.ts → types-Cz47Boks.d.ts} +6 -0
  107. package/dist/webhook.cjs.map +1 -1
  108. package/dist/webhook.d.cts +1 -1
  109. package/dist/webhook.d.ts +1 -1
  110. package/dist/webhook.js.map +1 -1
  111. 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-Di5HAWQz.cjs';
2
- export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-Di5HAWQz.cjs';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-BjBbCGeJ.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, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as StoredJob, T as TriageContext, n as TriageFn } from './types-BjBbCGeJ.cjs';
1
+ import { C as CronWatchOptions, a as CronWatch } from './client-D35cfnNV.cjs';
2
+ export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-D35cfnNV.cjs';
3
+ import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-Cz47Boks.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, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as StoredJob, T as TriageContext, n as TriageFn } from './types-Cz47Boks.cjs';
5
5
 
6
6
  /**
7
7
  * Keeps everything in process memory. The default when no store is given,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as CronWatchOptions, a as CronWatch } from './client-B5hM4l-D.js';
2
- export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-B5hM4l-D.js';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-BjBbCGeJ.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, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as StoredJob, T as TriageContext, n as TriageFn } from './types-BjBbCGeJ.js';
1
+ import { C as CronWatchOptions, a as CronWatch } from './client-CQm-8-mR.js';
2
+ export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-CQm-8-mR.js';
3
+ import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-Cz47Boks.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, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as StoredJob, T as TriageContext, n as TriageFn } from './types-Cz47Boks.js';
5
5
 
6
6
  /**
7
7
  * Keeps everything in process memory. The default when no store is given,
package/dist/index.js CHANGED
@@ -65,6 +65,14 @@ function formatRelative(at, now) {
65
65
  if (abs < 5e3) return text;
66
66
  return diff < 0 ? `${text} ago` : `in ${text}`;
67
67
  }
68
+ var FIRST_DATE_MS = -621355968e5;
69
+ var LAST_DATE_MS = 253402300799999;
70
+ function isoTime(at) {
71
+ return at >= FIRST_DATE_MS && at <= LAST_DATE_MS ? new Date(at).toISOString() : null;
72
+ }
73
+ function beyondDates(at) {
74
+ return at > LAST_DATE_MS ? "after 9999-12-31 23:59:59 UTC" : "before 0001-01-01 00:00:00 UTC";
75
+ }
68
76
 
69
77
  // src/env.ts
70
78
  function readEnv(name) {
@@ -102,15 +110,36 @@ function parseSchedule(schedule, timezone) {
102
110
  cache.set(key, parsed);
103
111
  return parsed;
104
112
  }
113
+ var CYCLE_MS = 146097 * 864e5;
114
+ var CRONER_FIRST_MS = Date.UTC(400, 0, 1);
115
+ var CRONER_LAST_MS = Date.UTC(2800, 0, 1);
116
+ function runsAfter(cron, n, from) {
117
+ let shift = 0;
118
+ if (from < CRONER_FIRST_MS) shift = Math.ceil((CRONER_FIRST_MS - from) / CYCLE_MS) * CYCLE_MS;
119
+ else if (from >= CRONER_LAST_MS) shift = -(Math.floor((from - CRONER_LAST_MS) / CYCLE_MS) + 1) * CYCLE_MS;
120
+ const out = [];
121
+ for (const date of cron.nextRuns(n, new Date(from + shift))) {
122
+ const t = date.getTime() - shift;
123
+ if (t > LAST_DATE_MS) break;
124
+ out.push(t);
125
+ }
126
+ return out;
127
+ }
128
+ function countFrom(from) {
129
+ if (from >= LAST_DATE_MS) return null;
130
+ return from >= FIRST_DATE_MS ? from : FIRST_DATE_MS - 1;
131
+ }
105
132
  function fireAfter(parsed, from) {
106
133
  const cron = crons.get(parsed);
107
134
  if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
108
- let probe = from;
135
+ const start = countFrom(from);
136
+ if (start === null) return null;
137
+ let probe = start;
109
138
  for (let attempt = 0; attempt < 4; attempt++) {
110
- const runs = cron.nextRuns(8, new Date(probe));
139
+ const runs = runsAfter(cron, 8, probe);
111
140
  if (runs.length === 0) return null;
112
- const found = runs.find((d) => d.getTime() > from);
113
- if (found) return found.getTime();
141
+ const found = runs.find((t) => t > start);
142
+ if (found !== void 0) return found;
114
143
  probe += 36e5;
115
144
  }
116
145
  return null;
@@ -119,20 +148,21 @@ function firesBetween(parsed, from, to, limit) {
119
148
  const cron = crons.get(parsed);
120
149
  if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
121
150
  const out = [];
122
- let probe = from;
123
- let last = from;
151
+ const start = countFrom(from);
152
+ if (start === null) return out;
153
+ let probe = start;
154
+ let last = start;
124
155
  for (let guard = 0; guard < 1e3; guard++) {
125
- const batch = cron.nextRuns(Math.min(limit + 1 - out.length, 24), new Date(probe));
156
+ const batch = runsAfter(cron, Math.min(limit + 1 - out.length, 24), probe);
126
157
  if (batch.length === 0) return out;
127
- for (const date of batch) {
128
- const t = date.getTime();
158
+ for (const t of batch) {
129
159
  if (t <= last) continue;
130
160
  if (t > to) return out;
131
161
  out.push(t);
132
162
  last = t;
133
163
  if (out.length > limit) return null;
134
164
  }
135
- const end = batch[batch.length - 1].getTime();
165
+ const end = batch[batch.length - 1];
136
166
  probe = end > probe ? end : probe + 36e5;
137
167
  }
138
168
  return out;
@@ -168,6 +198,7 @@ function runCovers(startedAt, dueAt, followingAt = null) {
168
198
  }
169
199
  function inSpringForwardGap(parsed, startedAt, fireAt) {
170
200
  const LOOKBACK = 3 * 36e5;
201
+ if (fireAt - LOOKBACK < FIRST_DATE_MS) return false;
171
202
  const after2 = utcOffset(fireAt, parsed.timezone);
172
203
  const before = utcOffset(fireAt - LOOKBACK, parsed.timezone);
173
204
  const gap = after2 - before;
@@ -204,8 +235,10 @@ function utcOffset(at, timezone) {
204
235
  for (const part of format.formatToParts(new Date(at))) {
205
236
  if (part.type !== "literal") parts[part.type] = Number(part.value);
206
237
  }
207
- const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
208
- return wall - Math.floor(at / 1e3) * 1e3;
238
+ const wall = /* @__PURE__ */ new Date(0);
239
+ wall.setUTCFullYear(parts.year, parts.month - 1, parts.day);
240
+ wall.setUTCHours(parts.hour, parts.minute, parts.second);
241
+ return wall.getTime() - Math.floor(at / 1e3) * 1e3;
209
242
  }
210
243
 
211
244
  // src/stats.ts
@@ -228,6 +261,20 @@ var DEFAULT_TIMEOUT_MS = 60 * 6e4;
228
261
  var SLOW_FLOOR_MS = 1e4;
229
262
  var BASELINE_MIN_RUNS = 5;
230
263
  var BASELINE_WINDOW = 20;
264
+ var MAX_DURATION_MS = Number.MAX_SAFE_INTEGER;
265
+ function runDuration(startedAt, finishedAt) {
266
+ const ms = finishedAt - startedAt;
267
+ return ms > 0 ? Math.min(ms, MAX_DURATION_MS) : 0;
268
+ }
269
+ function stateVersion(state) {
270
+ const version = state?.version;
271
+ return typeof version === "number" && Number.isSafeInteger(version) && version >= 0 ? version + 0 : 0;
272
+ }
273
+ function failureCount(state) {
274
+ const count = state?.consecutiveFailures;
275
+ if (typeof count !== "number" || !Number.isInteger(count) || count <= 0) return 0;
276
+ return Math.min(count, MAX_DURATION_MS);
277
+ }
231
278
  function emptyState(job) {
232
279
  return { job, open: {}, consecutiveFailures: 0, silencedUntil: null, lastAlertAt: null, pendingRecovery: [], undelivered: [] };
233
280
  }
@@ -237,6 +284,7 @@ function normalizeState(state, job) {
237
284
  ...emptyState(job),
238
285
  ...state,
239
286
  open: { ...state.open },
287
+ consecutiveFailures: failureCount(state),
240
288
  pendingRecovery: [...state.pendingRecovery ?? []],
241
289
  undelivered: [...state.undelivered ?? []]
242
290
  };
@@ -335,7 +383,7 @@ function onRunFinish(def, run, state, history, now) {
335
383
  }
336
384
  return { state: next, alerts };
337
385
  }
338
- next.consecutiveFailures += 1;
386
+ next.consecutiveFailures = Math.min(next.consecutiveFailures + 1, MAX_DURATION_MS);
339
387
  closeCondition(next, "missed");
340
388
  const threshold = Math.max(1, def.failuresBeforeAlert ?? 1);
341
389
  const condition = run.status === "timeout" ? "stuck" : "failed";
@@ -438,7 +486,9 @@ function summary(stored, recent, state, nextExpectedAt, health) {
438
486
  // src/format.ts
439
487
  function when(at, now) {
440
488
  if (at === null || at === void 0) return "never";
441
- return `${new Date(at).toISOString().replace("T", " ").slice(0, 19)} UTC (${formatRelative(at, now)})`;
489
+ const iso = isoTime(at);
490
+ if (iso === null) return beyondDates(at);
491
+ return `${iso.replace("T", " ").slice(0, 19)} UTC (${formatRelative(at, now)})`;
442
492
  }
443
493
  function firstLines(text, n) {
444
494
  if (!text) return "";
@@ -547,6 +597,9 @@ function capOutput(text) {
547
597
  function stripNul(text) {
548
598
  return text.includes("\0") ? text.replace(/\u0000/g, "") : text;
549
599
  }
600
+ function stripJsonNul(json2) {
601
+ return json2.includes("\\u0000") ? json2.replace(/\\(u0000|[\s\S])/g, (escape, next) => next === "u0000" ? "" : escape) : json2;
602
+ }
550
603
  function errorMessage(error) {
551
604
  return capOutput(describeError(error));
552
605
  }
@@ -834,9 +887,13 @@ function dayLabel(t) {
834
887
  return `${WEEKDAYS[d.getUTCDay()]} ${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]}`;
835
888
  }
836
889
  function when2(t, now) {
890
+ if (t > LAST_DATE_MS) return "after 31 Dec 9999 23:59";
891
+ if (!(t >= FIRST_DATE_MS)) return "before 1 Jan 0001 00:00";
837
892
  if (Math.floor(t / DAY) === Math.floor(now / DAY)) return clock(t);
838
893
  const d = new Date(t);
839
- return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} ${clock(t)}`;
894
+ const year = d.getUTCFullYear();
895
+ const other = year === new Date(now).getUTCFullYear() ? "" : ` ${String(year).padStart(4, "0")}`;
896
+ return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]}${other} ${clock(t)}`;
840
897
  }
841
898
  function parsedSchedule(job) {
842
899
  if (!job.definition.schedule) return null;
@@ -1327,7 +1384,8 @@ function sparkline(runs) {
1327
1384
  }
1328
1385
  function stamp(at, now) {
1329
1386
  if (at === null) return `<span class="muted">never</span>`;
1330
- const iso = new Date(at).toISOString();
1387
+ const iso = isoTime(at);
1388
+ if (iso === null) return `<span class="nowrap">${beyondDates(at)}</span>`;
1331
1389
  return `<time class="nowrap" datetime="${iso}" title="${iso.replace("T", " ").slice(0, 19)} UTC">${h(formatRelative(at, now))}</time>`;
1332
1390
  }
1333
1391
  function healthFigures(jobs) {
@@ -1794,13 +1852,21 @@ function memory() {
1794
1852
  const states = /* @__PURE__ */ new Map();
1795
1853
  let seq = 0;
1796
1854
  const clone = (v) => JSON.parse(JSON.stringify(v));
1855
+ const kept = (v) => JSON.parse(stripJsonNul(JSON.stringify(v)));
1856
+ const keptRun = (run) => ({
1857
+ ...clone(run),
1858
+ trigger: stripNul(run.trigger),
1859
+ output: run.output === null ? null : stripNul(run.output),
1860
+ error: run.error === null ? null : stripNul(run.error),
1861
+ metrics: kept(run.metrics)
1862
+ });
1797
1863
  const byName = (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
1798
1864
  return {
1799
1865
  async upsertJob(definition, now) {
1800
1866
  const existing = jobs.get(definition.name);
1801
1867
  jobs.set(definition.name, {
1802
1868
  name: definition.name,
1803
- definition: clone(definition),
1869
+ definition: kept(definition),
1804
1870
  createdAt: existing?.createdAt ?? now,
1805
1871
  updatedAt: now
1806
1872
  });
@@ -1822,19 +1888,19 @@ function memory() {
1822
1888
  },
1823
1889
  async insertRun(run) {
1824
1890
  if (runs.has(run.id)) throw new Error(`run ${run.id} already exists`);
1825
- runs.set(run.id, clone(run));
1891
+ runs.set(run.id, keptRun(run));
1826
1892
  order.set(run.id, ++seq);
1827
1893
  },
1828
1894
  async updateRun(run) {
1829
1895
  const existing = runs.get(run.id);
1830
1896
  if (!existing) return;
1831
- const { status, finishedAt, durationMs, error, output, metrics } = clone(run);
1897
+ const { status, finishedAt, durationMs, error, output, metrics } = keptRun(run);
1832
1898
  runs.set(run.id, { ...existing, status, finishedAt, durationMs, error, output, metrics });
1833
1899
  },
1834
1900
  async updateRunIf(run, fromStatuses) {
1835
1901
  const existing = runs.get(run.id);
1836
1902
  if (!existing || !fromStatuses.includes(existing.status)) return false;
1837
- const { status, finishedAt, durationMs, error, output, metrics } = clone(run);
1903
+ const { status, finishedAt, durationMs, error, output, metrics } = keptRun(run);
1838
1904
  runs.set(run.id, { ...existing, status, finishedAt, durationMs, error, output, metrics });
1839
1905
  return true;
1840
1906
  },
@@ -1857,11 +1923,11 @@ function memory() {
1857
1923
  return s ? clone(s) : null;
1858
1924
  },
1859
1925
  async setState(state) {
1860
- states.set(state.job, clone(state));
1926
+ states.set(state.job, kept(state));
1861
1927
  },
1862
1928
  async compareAndSetState(state, expectedVersion) {
1863
- if ((states.get(state.job)?.version ?? 0) !== expectedVersion) return false;
1864
- states.set(state.job, clone(state));
1929
+ if (stateVersion(states.get(state.job)) !== expectedVersion) return false;
1930
+ states.set(state.job, kept(state));
1865
1931
  return true;
1866
1932
  },
1867
1933
  async prune(before) {
@@ -1949,6 +2015,8 @@ var CronWatch = class {
1949
2015
  synced = /* @__PURE__ */ new Set();
1950
2016
  /** The tail of each job's queue of state updates. See serial(). */
1951
2017
  queues = /* @__PURE__ */ new Map();
2018
+ /** The tail of each job's queue of declaration writes. See sync(). */
2019
+ syncing = /* @__PURE__ */ new Map();
1952
2020
  /** start() calls with an id still in flight, so two at once in this process record one run. */
1953
2021
  starting = /* @__PURE__ */ new Map();
1954
2022
  ready = null;
@@ -2087,26 +2155,40 @@ var CronWatch = class {
2087
2155
  }
2088
2156
  await this.ready;
2089
2157
  }
2158
+ /**
2159
+ * Writes the declaration of `definition`'s name as it stands, unless the
2160
+ * store has it. A handle kept from an earlier declaration writes the one
2161
+ * that replaced it, never its own over it, and one forgotten since writes
2162
+ * its own. The writes of one name take turns, in the order they were asked
2163
+ * for, so one still under way cannot land after a later one; and a name
2164
+ * declared again while its write was under way is still to be written.
2165
+ */
2090
2166
  async sync(definition) {
2091
2167
  await this.ensureReady();
2092
- if (this.synced.has(definition.name)) return;
2093
- await this.store.upsertJob(toStored(definition), this.now());
2094
- this.synced.add(definition.name);
2168
+ const name = definition.name;
2169
+ if (this.synced.has(name)) return;
2170
+ await this.serial(name, async () => {
2171
+ if (this.synced.has(name)) return;
2172
+ const standing = this.definitions.get(name) ?? definition;
2173
+ await this.store.upsertJob(toStored(standing), this.now());
2174
+ if ((this.definitions.get(name) ?? standing) === standing) this.synced.add(name);
2175
+ }, this.syncing);
2095
2176
  }
2096
2177
  /**
2097
2178
  * Runs `fn` after every earlier state update for the same job has settled,
2098
2179
  * so two runs (or a run and a check) in this process never read and write
2099
2180
  * the job's state over each other. Other processes are coordinated by
2100
- * updateState() instead.
2181
+ * updateState() instead. sync() queues a job's declaration writes the same
2182
+ * way, in `syncing`.
2101
2183
  */
2102
- serial(job, fn) {
2103
- const result = (this.queues.get(job) ?? Promise.resolve()).then(fn);
2184
+ serial(job, fn, queues = this.queues) {
2185
+ const result = (queues.get(job) ?? Promise.resolve()).then(fn);
2104
2186
  const tail2 = result.then(() => {
2105
2187
  }, () => {
2106
2188
  });
2107
- this.queues.set(job, tail2);
2189
+ queues.set(job, tail2);
2108
2190
  void tail2.then(() => {
2109
- if (this.queues.get(job) === tail2) this.queues.delete(job);
2191
+ if (queues.get(job) === tail2) queues.delete(job);
2110
2192
  });
2111
2193
  return result;
2112
2194
  }
@@ -2130,7 +2212,7 @@ var CronWatch = class {
2130
2212
  const current = await this.readState(job);
2131
2213
  const { state, result } = await change(current);
2132
2214
  if (sameState(state, current)) return { state: current, result };
2133
- const version = current.version ?? 0;
2215
+ const version = stateVersion(current);
2134
2216
  const next = { ...state, version: version + 1 };
2135
2217
  if (await this.writeState(next, version)) return { state: next, result };
2136
2218
  if (attempt >= STATE_ATTEMPTS) {
@@ -2190,7 +2272,7 @@ var CronWatch = class {
2190
2272
  }
2191
2273
  const finishedAt = this.now();
2192
2274
  run.finishedAt = finishedAt;
2193
- run.durationMs = Math.max(0, finishedAt - startedAt);
2275
+ run.durationMs = runDuration(startedAt, finishedAt);
2194
2276
  run.metrics = recorder.metrics();
2195
2277
  run.output = recorder.output() ?? (typeof result === "string" ? capOutput(result) : null);
2196
2278
  this.conclude(definition, run, result, error, threw, recorder.expectText() ?? (typeof result === "string" ? result : null));
@@ -2424,7 +2506,7 @@ ${id}`;
2424
2506
  ...from,
2425
2507
  status: "running",
2426
2508
  finishedAt,
2427
- durationMs: Math.max(0, finishedAt - from.startedAt),
2509
+ durationMs: runDuration(from.startedAt, finishedAt),
2428
2510
  error: null,
2429
2511
  output: joinOutput(from.output, added),
2430
2512
  metrics: { ...from.metrics, ...recorder.metrics() }
@@ -2506,6 +2588,7 @@ ${id}`;
2506
2588
  async recordRun(input, options = {}) {
2507
2589
  const declared = this.definitions.get(input.job);
2508
2590
  if (!declared) throw new Error(`recordRun: job "${input.job}" is not declared; call job() first`);
2591
+ if (input.id.includes("\0")) throw new Error(`recordRun: run ids cannot contain a NUL character (job "${input.job}")`);
2509
2592
  await this.sync(declared);
2510
2593
  const run = { ...input, metrics: { ...input.metrics } };
2511
2594
  if (run.status === "ok") {
@@ -2613,7 +2696,7 @@ ${id}`;
2613
2696
  const timeout = timeoutMs(definition);
2614
2697
  run.status = "timeout";
2615
2698
  run.finishedAt = now;
2616
- run.durationMs = now - run.startedAt;
2699
+ run.durationMs = runDuration(run.startedAt, now);
2617
2700
  run.error = `Still running after ${formatDuration(timeout)}; marked as timed out`;
2618
2701
  if (!await this.writeRunIf(run, ["running"])) continue;
2619
2702
  alerts.push(...await this.finishRun(definition, run, now));
@@ -2876,6 +2959,7 @@ function checkRunId(job, id, method) {
2876
2959
  if (typeof id !== "string" || id.length === 0 || id.length > 200) {
2877
2960
  throw new Error(`job "${job}": ${method}() needs a run id of 1 to 200 characters (got ${typeof id === "string" ? `${id.length} characters` : typeof id})`);
2878
2961
  }
2962
+ if (id.includes("\0")) throw new Error(`job "${job}": ${method}() cannot take a run id containing a NUL character`);
2879
2963
  if (id.startsWith(RESERVED_RUN_ID_PREFIX)) {
2880
2964
  throw new Error(`job "${job}": ${method}() cannot take a run id starting with "${RESERVED_RUN_ID_PREFIX}", which the pg_cron source uses for its runs`);
2881
2965
  }