@cronwatch/sdk 0.1.0 → 0.3.1

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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -12
  3. package/dist/anthropic.cjs +19 -5
  4. package/dist/anthropic.cjs.map +1 -1
  5. package/dist/anthropic.d.cts +1 -1
  6. package/dist/anthropic.d.ts +1 -1
  7. package/dist/anthropic.js +19 -5
  8. package/dist/anthropic.js.map +1 -1
  9. package/dist/discord.cjs +14 -2
  10. package/dist/discord.cjs.map +1 -1
  11. package/dist/discord.d.cts +6 -2
  12. package/dist/discord.d.ts +6 -2
  13. package/dist/discord.js +13 -3
  14. package/dist/discord.js.map +1 -1
  15. package/dist/index.cjs +835 -293
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +133 -45
  18. package/dist/index.d.ts +133 -45
  19. package/dist/index.js +836 -291
  20. package/dist/index.js.map +1 -1
  21. package/dist/postgres.cjs +152 -73
  22. package/dist/postgres.cjs.map +1 -1
  23. package/dist/postgres.d.cts +2 -2
  24. package/dist/postgres.d.ts +2 -2
  25. package/dist/postgres.js +152 -73
  26. package/dist/postgres.js.map +1 -1
  27. package/dist/slack.cjs +12 -6
  28. package/dist/slack.cjs.map +1 -1
  29. package/dist/slack.d.cts +1 -1
  30. package/dist/slack.d.ts +1 -1
  31. package/dist/slack.js +12 -6
  32. package/dist/slack.js.map +1 -1
  33. package/dist/sqlite.cjs +184 -74
  34. package/dist/sqlite.cjs.map +1 -1
  35. package/dist/sqlite.d.cts +3 -1
  36. package/dist/sqlite.d.ts +3 -1
  37. package/dist/sqlite.js +185 -75
  38. package/dist/sqlite.js.map +1 -1
  39. package/dist/{types-BQ1P8z55.d.cts → types-DUt2apl0.d.cts} +84 -7
  40. package/dist/{types-BQ1P8z55.d.ts → types-DUt2apl0.d.ts} +84 -7
  41. package/dist/webhook.cjs +10 -2
  42. package/dist/webhook.cjs.map +1 -1
  43. package/dist/webhook.d.cts +1 -1
  44. package/dist/webhook.d.ts +1 -1
  45. package/dist/webhook.js +10 -2
  46. package/dist/webhook.js.map +1 -1
  47. package/package.json +97 -34
package/dist/index.d.cts CHANGED
@@ -1,6 +1,5 @@
1
- import { S as Store, a as AlertChannel, T as TriageFn, D as Duration, J as JobOptions, b as JobDefinition, R as Run, C as CheckResult, c as JobSummary, d as JobState, A as Alert, e as Condition, f as StoredJobDefinition } from './types-BQ1P8z55.cjs';
2
- export { g as AlertType, E as ExpectRule, h as JobHealth, i as RunStatus, j as StoredJob, k as TriageContext } from './types-BQ1P8z55.cjs';
3
- import { Cron } from 'croner';
1
+ import { S as Store, a as AlertChannel, T as TriageFn, D as Duration, J as JobOptions, b as JobDefinition, C as CheckResult, c as JobSummary, R as Run, d as JobState, A as Alert, e as AlertDraft, f as StoredJobDefinition } from './types-DUt2apl0.cjs';
2
+ export { g as AlertDetails, h as AlertType, B as BudgetBreach, i as Condition, E as ExpectRule, j as JobHealth, k as RunStatus, l as StoredJob, m as TriageContext } from './types-DUt2apl0.cjs';
4
3
 
5
4
  /** What a job function receives. */
6
5
  interface JobContext {
@@ -20,8 +19,11 @@ interface RoutesOptions {
20
19
  /**
21
20
  * Required to reach anything. Send it as `Authorization: Bearer <token>`,
22
21
  * or open the dashboard once with `?token=<token>` and a cookie is set.
23
- * Defaults to process.env.CRONWATCH_TOKEN. With no token at all, the routes
24
- * are open in development and refuse to serve in production.
22
+ * Defaults to process.env.CRONWATCH_TOKEN; an empty string counts as unset.
23
+ * With no token while NODE_ENV is "development" or "test", the routes make
24
+ * a random one and print a sign-in link to the server log on their first
25
+ * request; with no token otherwise they answer 503. Pass `null` to opt out
26
+ * and serve them open everywhere, for example behind your own auth.
25
27
  *
26
28
  * The check endpoint (/api/check) also accepts the client's cronSecret, so
27
29
  * a platform cron that sends `Authorization: Bearer <CRON_SECRET>` can
@@ -52,7 +54,9 @@ interface HandlerOptions {
52
54
  /**
53
55
  * Callers must send `Authorization: Bearer <secret>`. Defaults to the
54
56
  * client's cronSecret, which defaults to process.env.CRON_SECRET (what
55
- * Vercel sends its cron requests with). Pass null to allow anyone.
57
+ * Vercel sends its cron requests with). An empty string counts as unset.
58
+ * With no secret at all the handler answers 503 unless NODE_ENV is
59
+ * "development" or "test". Pass null to allow anyone.
56
60
  */
57
61
  secret?: string | null;
58
62
  }
@@ -71,41 +75,68 @@ interface CronWatchOptions {
71
75
  store?: Store;
72
76
  /** Where alerts go. Defaults to the console. */
73
77
  alerts?: AlertChannel[];
74
- /** Adds a short diagnosis to failure alerts. See @cronwatch/sdk/anthropic. */
78
+ /** Adds a short diagnosis to every alert except recoveries. See @cronwatch/sdk/anthropic. */
75
79
  triage?: TriageFn;
76
- /** Shared secret that handler() requests must carry. Defaults to process.env.CRON_SECRET. */
80
+ /**
81
+ * Shared secret that handler() requests must carry. Defaults to
82
+ * process.env.CRON_SECRET; an empty string counts as unset. Pass null to
83
+ * let handlers run without one.
84
+ */
77
85
  cronSecret?: string | null;
78
86
  /** How long finished runs are kept. Default "30d". */
79
87
  retention?: Duration;
80
88
  /** Applied to every job unless the job sets its own. */
81
89
  defaults?: Pick<JobOptions, "grace" | "timeout" | "timezone" | "failuresBeforeAlert">;
82
- /** Called with anything that goes wrong outside a job: an alert channel failing, a triage timeout. */
90
+ /**
91
+ * Applied to every run's output and error before it is stored, shown or
92
+ * sent to an alert channel or triage. The default blanks values that look
93
+ * like secrets (password=..., Authorization headers, URL credentials, bearer
94
+ * tokens, JWTs, PEM private keys, webhook URLs, AWS, GitHub, Slack, Stripe,
95
+ * Google and API key formats). Pass your own function, or false to keep
96
+ * output exactly as logged. A function that throws or returns something
97
+ * other than a string is reported to onError and the default is used.
98
+ */
99
+ redact?: ((text: string) => string) | false;
100
+ /**
101
+ * "now" (the default) sends alerts from this process. "check" sends nothing
102
+ * from here: each alert is queued in the store and the next check, in a
103
+ * process that delivers now, sends it (with triage). For a process that
104
+ * records runs but cannot reach the network, such as a sandboxed backup job.
105
+ * Its `alerts` and `triage` are not used.
106
+ */
107
+ deliver?: "now" | "check";
108
+ /** Called with anything that goes wrong outside a job: the store failing, an alert channel failing, a triage timeout. */
83
109
  onError?: (error: unknown, where: string) => void;
84
110
  /** The clock. Tests use this. */
85
111
  now?: () => number;
86
112
  }
87
- interface ExecuteResult<T> {
88
- run: Run;
89
- result: T | undefined;
90
- error: unknown;
91
- threw: boolean;
92
- }
93
113
  declare class CronWatch {
94
114
  readonly store: Store;
95
115
  readonly alerts: AlertChannel[];
96
116
  readonly triage: TriageFn | undefined;
117
+ /** The secret handler() requests must carry, or null when none is set. */
97
118
  readonly cronSecret: string | null;
98
119
  readonly retentionMs: number;
99
120
  readonly now: () => number;
100
121
  readonly onError: (error: unknown, where: string) => void;
122
+ /** cronSecret was passed as null: handlers may run without a secret. */
123
+ private readonly secretOptOut;
124
+ private readonly redact;
125
+ /** "check": queue alerts for another process's check instead of sending them. See CronWatchOptions.deliver. */
126
+ private readonly deferDelivery;
101
127
  private readonly defaults;
102
128
  private readonly definitions;
103
129
  private readonly synced;
130
+ /** The tail of each job's queue of state updates. See serial(). */
131
+ private readonly queues;
104
132
  private ready;
105
133
  private checking;
106
134
  private lastPruneAt;
107
135
  private timer;
136
+ private firstTick;
108
137
  private usingDefaultStore;
138
+ private warnedNoSecret;
139
+ private warnedDeferredStart;
109
140
  constructor(options?: CronWatchOptions);
110
141
  /** Declare a job. Call it once, at module level, and keep the handle. */
111
142
  job(name: string, options?: JobOptions): JobHandle;
@@ -115,25 +146,80 @@ declare class CronWatch {
115
146
  /** The definitions declared in this process. */
116
147
  definedJobs(): JobDefinition[];
117
148
  private handle;
149
+ private warnNoSecret;
150
+ /** onError, for places that must carry on even when onError itself throws. */
151
+ private report;
118
152
  private ensureReady;
119
153
  private sync;
120
- /** Runs a function as a recorded run. Never throws for the job's own error; see `threw`. */
121
- execute<T>(definition: JobDefinition, fn: JobFn<T>, trigger: string): Promise<ExecuteResult<T>>;
122
154
  /**
123
- * Look for missed and stuck runs across every job, send alerts, and prune
124
- * old runs. Call it from an interval (start()), a cron hitting the mounted
125
- * routes, or by hand. Concurrent calls share one check.
155
+ * Runs `fn` after every earlier state update for the same job has settled,
156
+ * so two runs (or a run and a check) in this process never read and write
157
+ * the job's state over each other. Other processes are coordinated by
158
+ * updateState() instead.
159
+ */
160
+ private serial;
161
+ private readState;
162
+ /**
163
+ * Every read-modify-write of a job's state goes through here. In turn with
164
+ * this process's other updates to the job (serial()), it reads the state,
165
+ * asks `change` for the next one, and writes it with the version one
166
+ * higher, only if the stored version is still the one read. When another
167
+ * process wrote in between, the write is refused and it starts again from
168
+ * a fresh read, up to STATE_ATTEMPTS times. So `change` may run more than
169
+ * once and must only compute: whatever it returns from the attempt that
170
+ * was written is the result. Nothing is written when the state is
171
+ * unchanged. Returns the state as stored.
172
+ */
173
+ private updateState;
174
+ /** A conditional write, or for a store without compareAndSetState, a plain one that always succeeds. */
175
+ private writeState;
176
+ /**
177
+ * Runs a function as a recorded run. The function always runs, whatever
178
+ * the store is doing: store errors go to onError, and the result is the
179
+ * function's own outcome. Never throws for the job's own error; see `threw`.
180
+ */
181
+ private execute;
182
+ /** Whether a check already marked this run as timed out, for a failure that finished late. */
183
+ private markedTimedOut;
184
+ /**
185
+ * Record a finished run (ok, failed, or timed out by a check), evaluate it
186
+ * against the job's state and send what that produces. Never throws.
187
+ */
188
+ private finishRun;
189
+ /**
190
+ * The runs before `run`, newest first, with up to BASELINE_WINDOW
191
+ * successful ones when the store has them. One small read normally; a
192
+ * larger one only when failures crowd the successes out of it.
193
+ */
194
+ private history;
195
+ /**
196
+ * Look for missed and stuck runs across every job, send alerts, retry
197
+ * alerts no channel accepted, and prune old runs. Call it from an interval
198
+ * (start()), a cron hitting the mounted routes, or by hand. Concurrent
199
+ * calls share one check.
126
200
  */
127
201
  check(): Promise<CheckResult>;
128
202
  private runCheck;
203
+ /** A job's summary and its newest runs, without alerting. A job that cannot be evaluated is reported and shown as failing. */
204
+ private snapshot;
205
+ /** The summary of a job whose evaluation failed, from whatever can still be read. */
206
+ private unevaluable;
129
207
  /** Every job the store knows about, with its health. Does not send alerts. */
130
208
  jobs(): Promise<JobSummary[]>;
209
+ /** Every job's summary with its newest `limit` runs, read together. What the dashboard shows. */
210
+ jobsWithRuns(limit?: number): Promise<{
211
+ job: JobSummary;
212
+ runs: Run[];
213
+ }[]>;
131
214
  jobSummary(name: string): Promise<JobSummary | null>;
215
+ /** A job's runs, newest first. `limit` is a whole number from 1 to 500. */
132
216
  runs(name: string, limit?: number): Promise<Run[]>;
133
217
  getRun(id: string): Promise<Run | null>;
134
218
  /** Stop alerts for a job for a while. State keeps updating underneath. */
135
219
  silence(name: string, duration: Duration): Promise<JobState>;
136
220
  unsilence(name: string): Promise<JobState>;
221
+ /** Read, change and write one job's state, in turn with every other update to it. */
222
+ private patchState;
137
223
  /** Remove a job and its runs from the store. A job still declared in code comes back on its next run. */
138
224
  forget(name: string): Promise<void>;
139
225
  /** The dashboard and JSON API as fetch-style handlers. See createRoutes(). */
@@ -142,18 +228,36 @@ declare class CronWatch {
142
228
  start(every?: Duration): void;
143
229
  stop(): void;
144
230
  close(): Promise<void>;
145
- private summarize;
146
- /** Save an evaluation's state, honouring silence, and send its alerts. */
147
- private settle;
231
+ /**
232
+ * Compose, triage and send each draft. The state was saved before this
233
+ * (updateState), so a slow channel holds up nothing else; afterwards only
234
+ * the delivery fields are written back, onto a fresh read of the state.
235
+ */
148
236
  private dispatch;
237
+ /**
238
+ * Send the alerts that no channel accepted last time, once each, oldest
239
+ * first. `state` is the job's state as this check left it: an alert that
240
+ * no longer describes it (staleAlert) is dropped instead. Retries across a
241
+ * check share RETRY_BUDGET_MS of wall-clock time; once it is spent the rest
242
+ * stay queued for the next check.
243
+ */
244
+ private retryUndelivered;
245
+ /**
246
+ * Mark delivered alerts done, drop stale ones, and keep failed ones for the
247
+ * next check. A failed alert replaces its stored copy, so a triage made on
248
+ * this attempt is kept. lastAlertAt moves only on a delivery.
249
+ */
250
+ private recordDelivery;
251
+ /** Send to every channel at once. True when at least one accepted it, or there are none. */
252
+ private deliver;
253
+ /** Sets alert.triage to the diagnosis, or to null when there is none, so it is tried once per alert. */
254
+ private addTriage;
149
255
  }
150
256
  /** Writes alerts to the console. The default channel. */
151
257
  declare function consoleChannel(): AlertChannel;
152
258
  /** Wrap any function as an alert channel. */
153
259
  declare function custom(name: string, send: (alert: Alert) => Promise<void> | void): AlertChannel;
154
260
 
155
- declare function json(body: unknown, status?: number, headers?: Record<string, string>): Response;
156
-
157
261
  /**
158
262
  * Keeps everything in process memory. The default when no store is given,
159
263
  * good for tests and for trying the library out. State is gone on restart,
@@ -168,15 +272,14 @@ declare function memory(): Store;
168
272
  declare function parseDuration(value: Duration, label?: string): number;
169
273
  /** 90000 -> "1m 30s". For messages, not for parsing back. */
170
274
  declare function formatDuration(ms: number): string;
171
- /** "5 minutes ago", "in 2h". Relative to `now`. */
172
- declare function formatRelative(at: number, now: number): string;
173
275
 
174
276
  interface ParsedSchedule {
175
277
  kind: "cron" | "interval";
176
278
  source: string;
279
+ /** The IANA timezone a cron is read in, when one was given. */
280
+ timezone?: string;
177
281
  /** For intervals, the period in milliseconds. */
178
282
  everyMs?: number;
179
- cron?: Cron;
180
283
  }
181
284
  /**
182
285
  * "0 2 * * *" (cron, five or six fields), "@hourly", or "every 5m".
@@ -187,23 +290,8 @@ interface ParsedSchedule {
187
290
  * timezone: "UTC" for those.
188
291
  */
189
292
  declare function parseSchedule(schedule: string, timezone?: string): ParsedSchedule;
190
- /** The next time the cron fires strictly after `from`. */
293
+ /** The next time the schedule fires strictly after `from`. For an interval, counted from the last run when there is one. */
191
294
  declare function nextFire(parsed: ParsedSchedule, from: number, lastRunAt: number | null): number | null;
192
- /**
193
- * The most recent time the cron fired at or before `now`, or null when the
194
- * expression never fires in the year before `now`.
195
- *
196
- * croner only looks forward, so this searches: widen a window behind `now`
197
- * until a fire time falls inside it, then bisect on the window's start for
198
- * the latest start whose next fire is still at or before `now`.
199
- */
200
- declare function previousFire(parsed: ParsedSchedule, now: number): number | null;
201
-
202
- interface AlertDraft {
203
- type: Condition | "recovered";
204
- run: Run | null;
205
- details: Record<string, unknown>;
206
- }
207
295
 
208
296
  /** Turns a draft into the title and message every channel shows. */
209
297
  declare function composeAlert(draft: AlertDraft, def: StoredJobDefinition, now: number): Alert;
@@ -216,4 +304,4 @@ declare function composeAlert(draft: AlertDraft, def: StoredJobDefinition, now:
216
304
  */
217
305
  declare function cronwatch(options?: CronWatchOptions): CronWatch;
218
306
 
219
- export { Alert, AlertChannel, CheckResult, Condition, CronWatch, type CronWatchOptions, Duration, type HandlerFn, type HandlerOptions, type JobContext, JobDefinition, type JobFn, type JobHandle, JobOptions, JobState, JobSummary, type Routes, type RoutesOptions, Run, Store, StoredJobDefinition, TriageFn, composeAlert, consoleChannel, createRoutes, cronwatch, custom, formatDuration, formatRelative, json, memory, nextFire, parseDuration, parseSchedule, previousFire };
307
+ export { Alert, AlertChannel, AlertDraft, CheckResult, CronWatch, type CronWatchOptions, Duration, type FetchHandler, type HandlerFn, type HandlerOptions, type JobContext, JobDefinition, type JobFn, type JobHandle, JobOptions, JobState, JobSummary, type ParsedSchedule, type Routes, type RoutesOptions, Run, Store, StoredJobDefinition, TriageFn, composeAlert, consoleChannel, createRoutes, cronwatch, custom, formatDuration, memory, nextFire, parseDuration, parseSchedule };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { S as Store, a as AlertChannel, T as TriageFn, D as Duration, J as JobOptions, b as JobDefinition, R as Run, C as CheckResult, c as JobSummary, d as JobState, A as Alert, e as Condition, f as StoredJobDefinition } from './types-BQ1P8z55.js';
2
- export { g as AlertType, E as ExpectRule, h as JobHealth, i as RunStatus, j as StoredJob, k as TriageContext } from './types-BQ1P8z55.js';
3
- import { Cron } from 'croner';
1
+ import { S as Store, a as AlertChannel, T as TriageFn, D as Duration, J as JobOptions, b as JobDefinition, C as CheckResult, c as JobSummary, R as Run, d as JobState, A as Alert, e as AlertDraft, f as StoredJobDefinition } from './types-DUt2apl0.js';
2
+ export { g as AlertDetails, h as AlertType, B as BudgetBreach, i as Condition, E as ExpectRule, j as JobHealth, k as RunStatus, l as StoredJob, m as TriageContext } from './types-DUt2apl0.js';
4
3
 
5
4
  /** What a job function receives. */
6
5
  interface JobContext {
@@ -20,8 +19,11 @@ interface RoutesOptions {
20
19
  /**
21
20
  * Required to reach anything. Send it as `Authorization: Bearer <token>`,
22
21
  * or open the dashboard once with `?token=<token>` and a cookie is set.
23
- * Defaults to process.env.CRONWATCH_TOKEN. With no token at all, the routes
24
- * are open in development and refuse to serve in production.
22
+ * Defaults to process.env.CRONWATCH_TOKEN; an empty string counts as unset.
23
+ * With no token while NODE_ENV is "development" or "test", the routes make
24
+ * a random one and print a sign-in link to the server log on their first
25
+ * request; with no token otherwise they answer 503. Pass `null` to opt out
26
+ * and serve them open everywhere, for example behind your own auth.
25
27
  *
26
28
  * The check endpoint (/api/check) also accepts the client's cronSecret, so
27
29
  * a platform cron that sends `Authorization: Bearer <CRON_SECRET>` can
@@ -52,7 +54,9 @@ interface HandlerOptions {
52
54
  /**
53
55
  * Callers must send `Authorization: Bearer <secret>`. Defaults to the
54
56
  * client's cronSecret, which defaults to process.env.CRON_SECRET (what
55
- * Vercel sends its cron requests with). Pass null to allow anyone.
57
+ * Vercel sends its cron requests with). An empty string counts as unset.
58
+ * With no secret at all the handler answers 503 unless NODE_ENV is
59
+ * "development" or "test". Pass null to allow anyone.
56
60
  */
57
61
  secret?: string | null;
58
62
  }
@@ -71,41 +75,68 @@ interface CronWatchOptions {
71
75
  store?: Store;
72
76
  /** Where alerts go. Defaults to the console. */
73
77
  alerts?: AlertChannel[];
74
- /** Adds a short diagnosis to failure alerts. See @cronwatch/sdk/anthropic. */
78
+ /** Adds a short diagnosis to every alert except recoveries. See @cronwatch/sdk/anthropic. */
75
79
  triage?: TriageFn;
76
- /** Shared secret that handler() requests must carry. Defaults to process.env.CRON_SECRET. */
80
+ /**
81
+ * Shared secret that handler() requests must carry. Defaults to
82
+ * process.env.CRON_SECRET; an empty string counts as unset. Pass null to
83
+ * let handlers run without one.
84
+ */
77
85
  cronSecret?: string | null;
78
86
  /** How long finished runs are kept. Default "30d". */
79
87
  retention?: Duration;
80
88
  /** Applied to every job unless the job sets its own. */
81
89
  defaults?: Pick<JobOptions, "grace" | "timeout" | "timezone" | "failuresBeforeAlert">;
82
- /** Called with anything that goes wrong outside a job: an alert channel failing, a triage timeout. */
90
+ /**
91
+ * Applied to every run's output and error before it is stored, shown or
92
+ * sent to an alert channel or triage. The default blanks values that look
93
+ * like secrets (password=..., Authorization headers, URL credentials, bearer
94
+ * tokens, JWTs, PEM private keys, webhook URLs, AWS, GitHub, Slack, Stripe,
95
+ * Google and API key formats). Pass your own function, or false to keep
96
+ * output exactly as logged. A function that throws or returns something
97
+ * other than a string is reported to onError and the default is used.
98
+ */
99
+ redact?: ((text: string) => string) | false;
100
+ /**
101
+ * "now" (the default) sends alerts from this process. "check" sends nothing
102
+ * from here: each alert is queued in the store and the next check, in a
103
+ * process that delivers now, sends it (with triage). For a process that
104
+ * records runs but cannot reach the network, such as a sandboxed backup job.
105
+ * Its `alerts` and `triage` are not used.
106
+ */
107
+ deliver?: "now" | "check";
108
+ /** Called with anything that goes wrong outside a job: the store failing, an alert channel failing, a triage timeout. */
83
109
  onError?: (error: unknown, where: string) => void;
84
110
  /** The clock. Tests use this. */
85
111
  now?: () => number;
86
112
  }
87
- interface ExecuteResult<T> {
88
- run: Run;
89
- result: T | undefined;
90
- error: unknown;
91
- threw: boolean;
92
- }
93
113
  declare class CronWatch {
94
114
  readonly store: Store;
95
115
  readonly alerts: AlertChannel[];
96
116
  readonly triage: TriageFn | undefined;
117
+ /** The secret handler() requests must carry, or null when none is set. */
97
118
  readonly cronSecret: string | null;
98
119
  readonly retentionMs: number;
99
120
  readonly now: () => number;
100
121
  readonly onError: (error: unknown, where: string) => void;
122
+ /** cronSecret was passed as null: handlers may run without a secret. */
123
+ private readonly secretOptOut;
124
+ private readonly redact;
125
+ /** "check": queue alerts for another process's check instead of sending them. See CronWatchOptions.deliver. */
126
+ private readonly deferDelivery;
101
127
  private readonly defaults;
102
128
  private readonly definitions;
103
129
  private readonly synced;
130
+ /** The tail of each job's queue of state updates. See serial(). */
131
+ private readonly queues;
104
132
  private ready;
105
133
  private checking;
106
134
  private lastPruneAt;
107
135
  private timer;
136
+ private firstTick;
108
137
  private usingDefaultStore;
138
+ private warnedNoSecret;
139
+ private warnedDeferredStart;
109
140
  constructor(options?: CronWatchOptions);
110
141
  /** Declare a job. Call it once, at module level, and keep the handle. */
111
142
  job(name: string, options?: JobOptions): JobHandle;
@@ -115,25 +146,80 @@ declare class CronWatch {
115
146
  /** The definitions declared in this process. */
116
147
  definedJobs(): JobDefinition[];
117
148
  private handle;
149
+ private warnNoSecret;
150
+ /** onError, for places that must carry on even when onError itself throws. */
151
+ private report;
118
152
  private ensureReady;
119
153
  private sync;
120
- /** Runs a function as a recorded run. Never throws for the job's own error; see `threw`. */
121
- execute<T>(definition: JobDefinition, fn: JobFn<T>, trigger: string): Promise<ExecuteResult<T>>;
122
154
  /**
123
- * Look for missed and stuck runs across every job, send alerts, and prune
124
- * old runs. Call it from an interval (start()), a cron hitting the mounted
125
- * routes, or by hand. Concurrent calls share one check.
155
+ * Runs `fn` after every earlier state update for the same job has settled,
156
+ * so two runs (or a run and a check) in this process never read and write
157
+ * the job's state over each other. Other processes are coordinated by
158
+ * updateState() instead.
159
+ */
160
+ private serial;
161
+ private readState;
162
+ /**
163
+ * Every read-modify-write of a job's state goes through here. In turn with
164
+ * this process's other updates to the job (serial()), it reads the state,
165
+ * asks `change` for the next one, and writes it with the version one
166
+ * higher, only if the stored version is still the one read. When another
167
+ * process wrote in between, the write is refused and it starts again from
168
+ * a fresh read, up to STATE_ATTEMPTS times. So `change` may run more than
169
+ * once and must only compute: whatever it returns from the attempt that
170
+ * was written is the result. Nothing is written when the state is
171
+ * unchanged. Returns the state as stored.
172
+ */
173
+ private updateState;
174
+ /** A conditional write, or for a store without compareAndSetState, a plain one that always succeeds. */
175
+ private writeState;
176
+ /**
177
+ * Runs a function as a recorded run. The function always runs, whatever
178
+ * the store is doing: store errors go to onError, and the result is the
179
+ * function's own outcome. Never throws for the job's own error; see `threw`.
180
+ */
181
+ private execute;
182
+ /** Whether a check already marked this run as timed out, for a failure that finished late. */
183
+ private markedTimedOut;
184
+ /**
185
+ * Record a finished run (ok, failed, or timed out by a check), evaluate it
186
+ * against the job's state and send what that produces. Never throws.
187
+ */
188
+ private finishRun;
189
+ /**
190
+ * The runs before `run`, newest first, with up to BASELINE_WINDOW
191
+ * successful ones when the store has them. One small read normally; a
192
+ * larger one only when failures crowd the successes out of it.
193
+ */
194
+ private history;
195
+ /**
196
+ * Look for missed and stuck runs across every job, send alerts, retry
197
+ * alerts no channel accepted, and prune old runs. Call it from an interval
198
+ * (start()), a cron hitting the mounted routes, or by hand. Concurrent
199
+ * calls share one check.
126
200
  */
127
201
  check(): Promise<CheckResult>;
128
202
  private runCheck;
203
+ /** A job's summary and its newest runs, without alerting. A job that cannot be evaluated is reported and shown as failing. */
204
+ private snapshot;
205
+ /** The summary of a job whose evaluation failed, from whatever can still be read. */
206
+ private unevaluable;
129
207
  /** Every job the store knows about, with its health. Does not send alerts. */
130
208
  jobs(): Promise<JobSummary[]>;
209
+ /** Every job's summary with its newest `limit` runs, read together. What the dashboard shows. */
210
+ jobsWithRuns(limit?: number): Promise<{
211
+ job: JobSummary;
212
+ runs: Run[];
213
+ }[]>;
131
214
  jobSummary(name: string): Promise<JobSummary | null>;
215
+ /** A job's runs, newest first. `limit` is a whole number from 1 to 500. */
132
216
  runs(name: string, limit?: number): Promise<Run[]>;
133
217
  getRun(id: string): Promise<Run | null>;
134
218
  /** Stop alerts for a job for a while. State keeps updating underneath. */
135
219
  silence(name: string, duration: Duration): Promise<JobState>;
136
220
  unsilence(name: string): Promise<JobState>;
221
+ /** Read, change and write one job's state, in turn with every other update to it. */
222
+ private patchState;
137
223
  /** Remove a job and its runs from the store. A job still declared in code comes back on its next run. */
138
224
  forget(name: string): Promise<void>;
139
225
  /** The dashboard and JSON API as fetch-style handlers. See createRoutes(). */
@@ -142,18 +228,36 @@ declare class CronWatch {
142
228
  start(every?: Duration): void;
143
229
  stop(): void;
144
230
  close(): Promise<void>;
145
- private summarize;
146
- /** Save an evaluation's state, honouring silence, and send its alerts. */
147
- private settle;
231
+ /**
232
+ * Compose, triage and send each draft. The state was saved before this
233
+ * (updateState), so a slow channel holds up nothing else; afterwards only
234
+ * the delivery fields are written back, onto a fresh read of the state.
235
+ */
148
236
  private dispatch;
237
+ /**
238
+ * Send the alerts that no channel accepted last time, once each, oldest
239
+ * first. `state` is the job's state as this check left it: an alert that
240
+ * no longer describes it (staleAlert) is dropped instead. Retries across a
241
+ * check share RETRY_BUDGET_MS of wall-clock time; once it is spent the rest
242
+ * stay queued for the next check.
243
+ */
244
+ private retryUndelivered;
245
+ /**
246
+ * Mark delivered alerts done, drop stale ones, and keep failed ones for the
247
+ * next check. A failed alert replaces its stored copy, so a triage made on
248
+ * this attempt is kept. lastAlertAt moves only on a delivery.
249
+ */
250
+ private recordDelivery;
251
+ /** Send to every channel at once. True when at least one accepted it, or there are none. */
252
+ private deliver;
253
+ /** Sets alert.triage to the diagnosis, or to null when there is none, so it is tried once per alert. */
254
+ private addTriage;
149
255
  }
150
256
  /** Writes alerts to the console. The default channel. */
151
257
  declare function consoleChannel(): AlertChannel;
152
258
  /** Wrap any function as an alert channel. */
153
259
  declare function custom(name: string, send: (alert: Alert) => Promise<void> | void): AlertChannel;
154
260
 
155
- declare function json(body: unknown, status?: number, headers?: Record<string, string>): Response;
156
-
157
261
  /**
158
262
  * Keeps everything in process memory. The default when no store is given,
159
263
  * good for tests and for trying the library out. State is gone on restart,
@@ -168,15 +272,14 @@ declare function memory(): Store;
168
272
  declare function parseDuration(value: Duration, label?: string): number;
169
273
  /** 90000 -> "1m 30s". For messages, not for parsing back. */
170
274
  declare function formatDuration(ms: number): string;
171
- /** "5 minutes ago", "in 2h". Relative to `now`. */
172
- declare function formatRelative(at: number, now: number): string;
173
275
 
174
276
  interface ParsedSchedule {
175
277
  kind: "cron" | "interval";
176
278
  source: string;
279
+ /** The IANA timezone a cron is read in, when one was given. */
280
+ timezone?: string;
177
281
  /** For intervals, the period in milliseconds. */
178
282
  everyMs?: number;
179
- cron?: Cron;
180
283
  }
181
284
  /**
182
285
  * "0 2 * * *" (cron, five or six fields), "@hourly", or "every 5m".
@@ -187,23 +290,8 @@ interface ParsedSchedule {
187
290
  * timezone: "UTC" for those.
188
291
  */
189
292
  declare function parseSchedule(schedule: string, timezone?: string): ParsedSchedule;
190
- /** The next time the cron fires strictly after `from`. */
293
+ /** The next time the schedule fires strictly after `from`. For an interval, counted from the last run when there is one. */
191
294
  declare function nextFire(parsed: ParsedSchedule, from: number, lastRunAt: number | null): number | null;
192
- /**
193
- * The most recent time the cron fired at or before `now`, or null when the
194
- * expression never fires in the year before `now`.
195
- *
196
- * croner only looks forward, so this searches: widen a window behind `now`
197
- * until a fire time falls inside it, then bisect on the window's start for
198
- * the latest start whose next fire is still at or before `now`.
199
- */
200
- declare function previousFire(parsed: ParsedSchedule, now: number): number | null;
201
-
202
- interface AlertDraft {
203
- type: Condition | "recovered";
204
- run: Run | null;
205
- details: Record<string, unknown>;
206
- }
207
295
 
208
296
  /** Turns a draft into the title and message every channel shows. */
209
297
  declare function composeAlert(draft: AlertDraft, def: StoredJobDefinition, now: number): Alert;
@@ -216,4 +304,4 @@ declare function composeAlert(draft: AlertDraft, def: StoredJobDefinition, now:
216
304
  */
217
305
  declare function cronwatch(options?: CronWatchOptions): CronWatch;
218
306
 
219
- export { Alert, AlertChannel, CheckResult, Condition, CronWatch, type CronWatchOptions, Duration, type HandlerFn, type HandlerOptions, type JobContext, JobDefinition, type JobFn, type JobHandle, JobOptions, JobState, JobSummary, type Routes, type RoutesOptions, Run, Store, StoredJobDefinition, TriageFn, composeAlert, consoleChannel, createRoutes, cronwatch, custom, formatDuration, formatRelative, json, memory, nextFire, parseDuration, parseSchedule, previousFire };
307
+ export { Alert, AlertChannel, AlertDraft, CheckResult, CronWatch, type CronWatchOptions, Duration, type FetchHandler, type HandlerFn, type HandlerOptions, type JobContext, JobDefinition, type JobFn, type JobHandle, JobOptions, JobState, JobSummary, type ParsedSchedule, type Routes, type RoutesOptions, Run, Store, StoredJobDefinition, TriageFn, composeAlert, consoleChannel, createRoutes, cronwatch, custom, formatDuration, memory, nextFire, parseDuration, parseSchedule };