@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.cjs CHANGED
@@ -67,6 +67,14 @@ function formatRelative(at, now) {
67
67
  if (abs < 5e3) return text;
68
68
  return diff < 0 ? `${text} ago` : `in ${text}`;
69
69
  }
70
+ var FIRST_DATE_MS = -621355968e5;
71
+ var LAST_DATE_MS = 253402300799999;
72
+ function isoTime(at) {
73
+ return at >= FIRST_DATE_MS && at <= LAST_DATE_MS ? new Date(at).toISOString() : null;
74
+ }
75
+ function beyondDates(at) {
76
+ return at > LAST_DATE_MS ? "after 9999-12-31 23:59:59 UTC" : "before 0001-01-01 00:00:00 UTC";
77
+ }
70
78
 
71
79
  // src/env.ts
72
80
  function readEnv(name) {
@@ -104,15 +112,36 @@ function parseSchedule(schedule, timezone) {
104
112
  cache.set(key, parsed);
105
113
  return parsed;
106
114
  }
115
+ var CYCLE_MS = 146097 * 864e5;
116
+ var CRONER_FIRST_MS = Date.UTC(400, 0, 1);
117
+ var CRONER_LAST_MS = Date.UTC(2800, 0, 1);
118
+ function runsAfter(cron, n, from) {
119
+ let shift = 0;
120
+ if (from < CRONER_FIRST_MS) shift = Math.ceil((CRONER_FIRST_MS - from) / CYCLE_MS) * CYCLE_MS;
121
+ else if (from >= CRONER_LAST_MS) shift = -(Math.floor((from - CRONER_LAST_MS) / CYCLE_MS) + 1) * CYCLE_MS;
122
+ const out = [];
123
+ for (const date of cron.nextRuns(n, new Date(from + shift))) {
124
+ const t = date.getTime() - shift;
125
+ if (t > LAST_DATE_MS) break;
126
+ out.push(t);
127
+ }
128
+ return out;
129
+ }
130
+ function countFrom(from) {
131
+ if (from >= LAST_DATE_MS) return null;
132
+ return from >= FIRST_DATE_MS ? from : FIRST_DATE_MS - 1;
133
+ }
107
134
  function fireAfter(parsed, from) {
108
135
  const cron = crons.get(parsed);
109
136
  if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
110
- let probe = from;
137
+ const start = countFrom(from);
138
+ if (start === null) return null;
139
+ let probe = start;
111
140
  for (let attempt = 0; attempt < 4; attempt++) {
112
- const runs = cron.nextRuns(8, new Date(probe));
141
+ const runs = runsAfter(cron, 8, probe);
113
142
  if (runs.length === 0) return null;
114
- const found = runs.find((d) => d.getTime() > from);
115
- if (found) return found.getTime();
143
+ const found = runs.find((t) => t > start);
144
+ if (found !== void 0) return found;
116
145
  probe += 36e5;
117
146
  }
118
147
  return null;
@@ -121,20 +150,21 @@ function firesBetween(parsed, from, to, limit) {
121
150
  const cron = crons.get(parsed);
122
151
  if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
123
152
  const out = [];
124
- let probe = from;
125
- let last = from;
153
+ const start = countFrom(from);
154
+ if (start === null) return out;
155
+ let probe = start;
156
+ let last = start;
126
157
  for (let guard = 0; guard < 1e3; guard++) {
127
- const batch = cron.nextRuns(Math.min(limit + 1 - out.length, 24), new Date(probe));
158
+ const batch = runsAfter(cron, Math.min(limit + 1 - out.length, 24), probe);
128
159
  if (batch.length === 0) return out;
129
- for (const date of batch) {
130
- const t = date.getTime();
160
+ for (const t of batch) {
131
161
  if (t <= last) continue;
132
162
  if (t > to) return out;
133
163
  out.push(t);
134
164
  last = t;
135
165
  if (out.length > limit) return null;
136
166
  }
137
- const end = batch[batch.length - 1].getTime();
167
+ const end = batch[batch.length - 1];
138
168
  probe = end > probe ? end : probe + 36e5;
139
169
  }
140
170
  return out;
@@ -170,6 +200,7 @@ function runCovers(startedAt, dueAt, followingAt = null) {
170
200
  }
171
201
  function inSpringForwardGap(parsed, startedAt, fireAt) {
172
202
  const LOOKBACK = 3 * 36e5;
203
+ if (fireAt - LOOKBACK < FIRST_DATE_MS) return false;
173
204
  const after2 = utcOffset(fireAt, parsed.timezone);
174
205
  const before = utcOffset(fireAt - LOOKBACK, parsed.timezone);
175
206
  const gap = after2 - before;
@@ -206,8 +237,10 @@ function utcOffset(at, timezone) {
206
237
  for (const part of format.formatToParts(new Date(at))) {
207
238
  if (part.type !== "literal") parts[part.type] = Number(part.value);
208
239
  }
209
- const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
210
- return wall - Math.floor(at / 1e3) * 1e3;
240
+ const wall = /* @__PURE__ */ new Date(0);
241
+ wall.setUTCFullYear(parts.year, parts.month - 1, parts.day);
242
+ wall.setUTCHours(parts.hour, parts.minute, parts.second);
243
+ return wall.getTime() - Math.floor(at / 1e3) * 1e3;
211
244
  }
212
245
 
213
246
  // src/stats.ts
@@ -230,6 +263,20 @@ var DEFAULT_TIMEOUT_MS = 60 * 6e4;
230
263
  var SLOW_FLOOR_MS = 1e4;
231
264
  var BASELINE_MIN_RUNS = 5;
232
265
  var BASELINE_WINDOW = 20;
266
+ var MAX_DURATION_MS = Number.MAX_SAFE_INTEGER;
267
+ function runDuration(startedAt, finishedAt) {
268
+ const ms = finishedAt - startedAt;
269
+ return ms > 0 ? Math.min(ms, MAX_DURATION_MS) : 0;
270
+ }
271
+ function stateVersion(state) {
272
+ const version = state?.version;
273
+ return typeof version === "number" && Number.isSafeInteger(version) && version >= 0 ? version + 0 : 0;
274
+ }
275
+ function failureCount(state) {
276
+ const count = state?.consecutiveFailures;
277
+ if (typeof count !== "number" || !Number.isInteger(count) || count <= 0) return 0;
278
+ return Math.min(count, MAX_DURATION_MS);
279
+ }
233
280
  function emptyState(job) {
234
281
  return { job, open: {}, consecutiveFailures: 0, silencedUntil: null, lastAlertAt: null, pendingRecovery: [], undelivered: [] };
235
282
  }
@@ -239,6 +286,7 @@ function normalizeState(state, job) {
239
286
  ...emptyState(job),
240
287
  ...state,
241
288
  open: { ...state.open },
289
+ consecutiveFailures: failureCount(state),
242
290
  pendingRecovery: [...state.pendingRecovery ?? []],
243
291
  undelivered: [...state.undelivered ?? []]
244
292
  };
@@ -337,7 +385,7 @@ function onRunFinish(def, run, state, history, now) {
337
385
  }
338
386
  return { state: next, alerts };
339
387
  }
340
- next.consecutiveFailures += 1;
388
+ next.consecutiveFailures = Math.min(next.consecutiveFailures + 1, MAX_DURATION_MS);
341
389
  closeCondition(next, "missed");
342
390
  const threshold = Math.max(1, def.failuresBeforeAlert ?? 1);
343
391
  const condition = run.status === "timeout" ? "stuck" : "failed";
@@ -440,7 +488,9 @@ function summary(stored, recent, state, nextExpectedAt, health) {
440
488
  // src/format.ts
441
489
  function when(at, now) {
442
490
  if (at === null || at === void 0) return "never";
443
- return `${new Date(at).toISOString().replace("T", " ").slice(0, 19)} UTC (${formatRelative(at, now)})`;
491
+ const iso = isoTime(at);
492
+ if (iso === null) return beyondDates(at);
493
+ return `${iso.replace("T", " ").slice(0, 19)} UTC (${formatRelative(at, now)})`;
444
494
  }
445
495
  function firstLines(text, n) {
446
496
  if (!text) return "";
@@ -549,6 +599,9 @@ function capOutput(text) {
549
599
  function stripNul(text) {
550
600
  return text.includes("\0") ? text.replace(/\u0000/g, "") : text;
551
601
  }
602
+ function stripJsonNul(json2) {
603
+ return json2.includes("\\u0000") ? json2.replace(/\\(u0000|[\s\S])/g, (escape, next) => next === "u0000" ? "" : escape) : json2;
604
+ }
552
605
  function errorMessage(error) {
553
606
  return capOutput(describeError(error));
554
607
  }
@@ -836,9 +889,13 @@ function dayLabel(t) {
836
889
  return `${WEEKDAYS[d.getUTCDay()]} ${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]}`;
837
890
  }
838
891
  function when2(t, now) {
892
+ if (t > LAST_DATE_MS) return "after 31 Dec 9999 23:59";
893
+ if (!(t >= FIRST_DATE_MS)) return "before 1 Jan 0001 00:00";
839
894
  if (Math.floor(t / DAY) === Math.floor(now / DAY)) return clock(t);
840
895
  const d = new Date(t);
841
- return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} ${clock(t)}`;
896
+ const year = d.getUTCFullYear();
897
+ const other = year === new Date(now).getUTCFullYear() ? "" : ` ${String(year).padStart(4, "0")}`;
898
+ return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]}${other} ${clock(t)}`;
842
899
  }
843
900
  function parsedSchedule(job) {
844
901
  if (!job.definition.schedule) return null;
@@ -1329,7 +1386,8 @@ function sparkline(runs) {
1329
1386
  }
1330
1387
  function stamp(at, now) {
1331
1388
  if (at === null) return `<span class="muted">never</span>`;
1332
- const iso = new Date(at).toISOString();
1389
+ const iso = isoTime(at);
1390
+ if (iso === null) return `<span class="nowrap">${beyondDates(at)}</span>`;
1333
1391
  return `<time class="nowrap" datetime="${iso}" title="${iso.replace("T", " ").slice(0, 19)} UTC">${h(formatRelative(at, now))}</time>`;
1334
1392
  }
1335
1393
  function healthFigures(jobs) {
@@ -1796,13 +1854,21 @@ function memory() {
1796
1854
  const states = /* @__PURE__ */ new Map();
1797
1855
  let seq = 0;
1798
1856
  const clone = (v) => JSON.parse(JSON.stringify(v));
1857
+ const kept = (v) => JSON.parse(stripJsonNul(JSON.stringify(v)));
1858
+ const keptRun = (run) => ({
1859
+ ...clone(run),
1860
+ trigger: stripNul(run.trigger),
1861
+ output: run.output === null ? null : stripNul(run.output),
1862
+ error: run.error === null ? null : stripNul(run.error),
1863
+ metrics: kept(run.metrics)
1864
+ });
1799
1865
  const byName = (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
1800
1866
  return {
1801
1867
  async upsertJob(definition, now) {
1802
1868
  const existing = jobs.get(definition.name);
1803
1869
  jobs.set(definition.name, {
1804
1870
  name: definition.name,
1805
- definition: clone(definition),
1871
+ definition: kept(definition),
1806
1872
  createdAt: existing?.createdAt ?? now,
1807
1873
  updatedAt: now
1808
1874
  });
@@ -1824,19 +1890,19 @@ function memory() {
1824
1890
  },
1825
1891
  async insertRun(run) {
1826
1892
  if (runs.has(run.id)) throw new Error(`run ${run.id} already exists`);
1827
- runs.set(run.id, clone(run));
1893
+ runs.set(run.id, keptRun(run));
1828
1894
  order.set(run.id, ++seq);
1829
1895
  },
1830
1896
  async updateRun(run) {
1831
1897
  const existing = runs.get(run.id);
1832
1898
  if (!existing) return;
1833
- const { status, finishedAt, durationMs, error, output, metrics } = clone(run);
1899
+ const { status, finishedAt, durationMs, error, output, metrics } = keptRun(run);
1834
1900
  runs.set(run.id, { ...existing, status, finishedAt, durationMs, error, output, metrics });
1835
1901
  },
1836
1902
  async updateRunIf(run, fromStatuses) {
1837
1903
  const existing = runs.get(run.id);
1838
1904
  if (!existing || !fromStatuses.includes(existing.status)) return false;
1839
- const { status, finishedAt, durationMs, error, output, metrics } = clone(run);
1905
+ const { status, finishedAt, durationMs, error, output, metrics } = keptRun(run);
1840
1906
  runs.set(run.id, { ...existing, status, finishedAt, durationMs, error, output, metrics });
1841
1907
  return true;
1842
1908
  },
@@ -1859,11 +1925,11 @@ function memory() {
1859
1925
  return s ? clone(s) : null;
1860
1926
  },
1861
1927
  async setState(state) {
1862
- states.set(state.job, clone(state));
1928
+ states.set(state.job, kept(state));
1863
1929
  },
1864
1930
  async compareAndSetState(state, expectedVersion) {
1865
- if ((states.get(state.job)?.version ?? 0) !== expectedVersion) return false;
1866
- states.set(state.job, clone(state));
1931
+ if (stateVersion(states.get(state.job)) !== expectedVersion) return false;
1932
+ states.set(state.job, kept(state));
1867
1933
  return true;
1868
1934
  },
1869
1935
  async prune(before) {
@@ -1951,6 +2017,8 @@ var CronWatch = class {
1951
2017
  synced = /* @__PURE__ */ new Set();
1952
2018
  /** The tail of each job's queue of state updates. See serial(). */
1953
2019
  queues = /* @__PURE__ */ new Map();
2020
+ /** The tail of each job's queue of declaration writes. See sync(). */
2021
+ syncing = /* @__PURE__ */ new Map();
1954
2022
  /** start() calls with an id still in flight, so two at once in this process record one run. */
1955
2023
  starting = /* @__PURE__ */ new Map();
1956
2024
  ready = null;
@@ -2089,26 +2157,40 @@ var CronWatch = class {
2089
2157
  }
2090
2158
  await this.ready;
2091
2159
  }
2160
+ /**
2161
+ * Writes the declaration of `definition`'s name as it stands, unless the
2162
+ * store has it. A handle kept from an earlier declaration writes the one
2163
+ * that replaced it, never its own over it, and one forgotten since writes
2164
+ * its own. The writes of one name take turns, in the order they were asked
2165
+ * for, so one still under way cannot land after a later one; and a name
2166
+ * declared again while its write was under way is still to be written.
2167
+ */
2092
2168
  async sync(definition) {
2093
2169
  await this.ensureReady();
2094
- if (this.synced.has(definition.name)) return;
2095
- await this.store.upsertJob(toStored(definition), this.now());
2096
- this.synced.add(definition.name);
2170
+ const name = definition.name;
2171
+ if (this.synced.has(name)) return;
2172
+ await this.serial(name, async () => {
2173
+ if (this.synced.has(name)) return;
2174
+ const standing = this.definitions.get(name) ?? definition;
2175
+ await this.store.upsertJob(toStored(standing), this.now());
2176
+ if ((this.definitions.get(name) ?? standing) === standing) this.synced.add(name);
2177
+ }, this.syncing);
2097
2178
  }
2098
2179
  /**
2099
2180
  * Runs `fn` after every earlier state update for the same job has settled,
2100
2181
  * so two runs (or a run and a check) in this process never read and write
2101
2182
  * the job's state over each other. Other processes are coordinated by
2102
- * updateState() instead.
2183
+ * updateState() instead. sync() queues a job's declaration writes the same
2184
+ * way, in `syncing`.
2103
2185
  */
2104
- serial(job, fn) {
2105
- const result = (this.queues.get(job) ?? Promise.resolve()).then(fn);
2186
+ serial(job, fn, queues = this.queues) {
2187
+ const result = (queues.get(job) ?? Promise.resolve()).then(fn);
2106
2188
  const tail2 = result.then(() => {
2107
2189
  }, () => {
2108
2190
  });
2109
- this.queues.set(job, tail2);
2191
+ queues.set(job, tail2);
2110
2192
  void tail2.then(() => {
2111
- if (this.queues.get(job) === tail2) this.queues.delete(job);
2193
+ if (queues.get(job) === tail2) queues.delete(job);
2112
2194
  });
2113
2195
  return result;
2114
2196
  }
@@ -2132,7 +2214,7 @@ var CronWatch = class {
2132
2214
  const current = await this.readState(job);
2133
2215
  const { state, result } = await change(current);
2134
2216
  if (sameState(state, current)) return { state: current, result };
2135
- const version = current.version ?? 0;
2217
+ const version = stateVersion(current);
2136
2218
  const next = { ...state, version: version + 1 };
2137
2219
  if (await this.writeState(next, version)) return { state: next, result };
2138
2220
  if (attempt >= STATE_ATTEMPTS) {
@@ -2192,7 +2274,7 @@ var CronWatch = class {
2192
2274
  }
2193
2275
  const finishedAt = this.now();
2194
2276
  run.finishedAt = finishedAt;
2195
- run.durationMs = Math.max(0, finishedAt - startedAt);
2277
+ run.durationMs = runDuration(startedAt, finishedAt);
2196
2278
  run.metrics = recorder.metrics();
2197
2279
  run.output = recorder.output() ?? (typeof result === "string" ? capOutput(result) : null);
2198
2280
  this.conclude(definition, run, result, error, threw, recorder.expectText() ?? (typeof result === "string" ? result : null));
@@ -2426,7 +2508,7 @@ ${id}`;
2426
2508
  ...from,
2427
2509
  status: "running",
2428
2510
  finishedAt,
2429
- durationMs: Math.max(0, finishedAt - from.startedAt),
2511
+ durationMs: runDuration(from.startedAt, finishedAt),
2430
2512
  error: null,
2431
2513
  output: joinOutput(from.output, added),
2432
2514
  metrics: { ...from.metrics, ...recorder.metrics() }
@@ -2508,6 +2590,7 @@ ${id}`;
2508
2590
  async recordRun(input, options = {}) {
2509
2591
  const declared = this.definitions.get(input.job);
2510
2592
  if (!declared) throw new Error(`recordRun: job "${input.job}" is not declared; call job() first`);
2593
+ if (input.id.includes("\0")) throw new Error(`recordRun: run ids cannot contain a NUL character (job "${input.job}")`);
2511
2594
  await this.sync(declared);
2512
2595
  const run = { ...input, metrics: { ...input.metrics } };
2513
2596
  if (run.status === "ok") {
@@ -2615,7 +2698,7 @@ ${id}`;
2615
2698
  const timeout = timeoutMs(definition);
2616
2699
  run.status = "timeout";
2617
2700
  run.finishedAt = now;
2618
- run.durationMs = now - run.startedAt;
2701
+ run.durationMs = runDuration(run.startedAt, now);
2619
2702
  run.error = `Still running after ${formatDuration(timeout)}; marked as timed out`;
2620
2703
  if (!await this.writeRunIf(run, ["running"])) continue;
2621
2704
  alerts.push(...await this.finishRun(definition, run, now));
@@ -2878,6 +2961,7 @@ function checkRunId(job, id, method) {
2878
2961
  if (typeof id !== "string" || id.length === 0 || id.length > 200) {
2879
2962
  throw new Error(`job "${job}": ${method}() needs a run id of 1 to 200 characters (got ${typeof id === "string" ? `${id.length} characters` : typeof id})`);
2880
2963
  }
2964
+ if (id.includes("\0")) throw new Error(`job "${job}": ${method}() cannot take a run id containing a NUL character`);
2881
2965
  if (id.startsWith(RESERVED_RUN_ID_PREFIX)) {
2882
2966
  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`);
2883
2967
  }