@cronwatch/sdk 0.14.3 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CronwatchOptions, a as Cronwatch } from './client-CHjrNHH-.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-CHjrNHH-.cjs';
1
+ import { C as CronwatchOptions, a as Cronwatch } from './client-IuAn2-v9.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-IuAn2-v9.cjs';
3
3
  import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-B8IFVK5k.cjs';
4
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 HeldAlert, 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-B8IFVK5k.cjs';
5
5
 
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
11
11
  declare function memory(): Store & Required<Pick<Store, "lastRun">>;
12
12
 
13
13
  /** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
14
- declare const VERSION = "0.14.3";
14
+ declare const VERSION = "0.15.0";
15
15
 
16
16
  /**
17
17
  * "15m" -> 900000. Accepts a plain number of milliseconds, and compound
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CronwatchOptions, a as Cronwatch } from './client-C8j7B2Bo.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-C8j7B2Bo.js';
1
+ import { C as CronwatchOptions, a as Cronwatch } from './client-MLN3rDFE.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-MLN3rDFE.js';
3
3
  import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-B8IFVK5k.js';
4
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 HeldAlert, 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-B8IFVK5k.js';
5
5
 
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
11
11
  declare function memory(): Store & Required<Pick<Store, "lastRun">>;
12
12
 
13
13
  /** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
14
- declare const VERSION = "0.14.3";
14
+ declare const VERSION = "0.15.0";
15
15
 
16
16
  /**
17
17
  * "15m" -> 900000. Accepts a plain number of milliseconds, and compound
package/dist/index.js CHANGED
@@ -634,7 +634,6 @@ function catchesUp(def, run, now) {
634
634
  function onRunStart(def, run, state, now) {
635
635
  const next = cloneState(state);
636
636
  if (catchesUp(def, run, now)) closeCondition(next, "missed");
637
- closeCondition(next, "stuck");
638
637
  return next;
639
638
  }
640
639
  function onRunFinish(def, run, state, history, now) {
@@ -1251,7 +1250,7 @@ function staticAsset(path, base) {
1251
1250
  }
1252
1251
 
1253
1252
  // src/version.ts
1254
- var VERSION = "0.14.3";
1253
+ var VERSION = "0.15.0";
1255
1254
  var API_VERSION = 1;
1256
1255
 
1257
1256
  // src/routes/timeline.ts
@@ -2438,6 +2437,7 @@ var TRIAGE_TIMEOUT_MS = 25e3;
2438
2437
  var CHANNEL_TIMEOUT_MS = 15e3;
2439
2438
  var PRUNE_INTERVAL_MS = 60 * 6e4;
2440
2439
  var RETRY_BUDGET_MS = 2e4;
2440
+ var START_DEADLINE_MS = 5e3;
2441
2441
  var STATE_ATTEMPTS = 10;
2442
2442
  var HISTORY_PAGE = BASELINE_WINDOW + 5;
2443
2443
  var HISTORY_MAX = 200;
@@ -2681,8 +2681,8 @@ var Cronwatch = class {
2681
2681
  /**
2682
2682
  * Writes the declaration of `definition`'s name as it stands, unless the
2683
2683
  * store has it. A handle kept from an earlier declaration writes the one
2684
- * that replaced it, never its own over it, and one forgotten since writes
2685
- * its own. The writes of one name take turns, in the order they were asked
2684
+ * that replaced it, never its own over it, and one forgotten since
2685
+ * declares the job again as it was last declared here. The writes of one name take turns, in the order they were asked
2686
2686
  * for, so one still under way cannot land after a later one; and a name
2687
2687
  * declared again while its write was under way is still to be written. A
2688
2688
  * name is marked as written only while that same declaration stands, so a
@@ -2691,9 +2691,12 @@ var Cronwatch = class {
2691
2691
  *
2692
2692
  * With `confirm`, as a run starts, a name already written is read back:
2693
2693
  * another process may have forgotten the job since, and a job still
2694
- * declared here comes back on its next run.
2694
+ * declared here comes back on its next run. `running` says the write is
2695
+ * for a run (by default, whenever `confirm` is): a name forgotten here is
2696
+ * then declared again, where a check or a dashboard read, whose loop may
2697
+ * have picked the name up just before the forget, leaves it forgotten.
2695
2698
  */
2696
- async sync(definition, confirm = false) {
2699
+ async sync(definition, confirm = false, running = confirm) {
2697
2700
  await this.ensureReady();
2698
2701
  const name = definition.name;
2699
2702
  if (this.synced.has(name)) {
@@ -2702,6 +2705,10 @@ var Cronwatch = class {
2702
2705
  }
2703
2706
  await this.serial(name, async () => {
2704
2707
  if (this.synced.has(name)) return;
2708
+ if (!this.definitions.has(name)) {
2709
+ if (!running) return;
2710
+ this.declared(name);
2711
+ }
2705
2712
  const standing = this.definitions.get(name) ?? definition;
2706
2713
  await this.store.upsertJob(toStored(standing), this.now());
2707
2714
  if (this.definitions.get(name) === standing) this.synced.add(name);
@@ -2809,15 +2816,32 @@ var Cronwatch = class {
2809
2816
  metrics: {},
2810
2817
  trigger
2811
2818
  };
2819
+ const begun = { ...run };
2812
2820
  let recorded = false;
2813
- try {
2821
+ let landedLate = false;
2822
+ let concluded = false;
2823
+ const writing = (async () => {
2814
2824
  await this.sync(definition, true);
2815
- await this.store.insertRun({ ...run });
2816
- recorded = true;
2825
+ await this.store.insertRun({ ...begun });
2826
+ })();
2827
+ try {
2828
+ if (await within(writing, START_DEADLINE_MS) === TIMED_OUT) {
2829
+ this.report(new Error(`the store took longer than ${formatDuration(START_DEADLINE_MS)} to record the start of ${name}; running it unrecorded`), `recording ${name}`);
2830
+ void writing.then(
2831
+ () => {
2832
+ landedLate = true;
2833
+ if (concluded) return this.recordFinish(definition, run, true, run.finishedAt).then(() => {
2834
+ }, (e) => this.report(e, `recording ${name}`));
2835
+ },
2836
+ () => {
2837
+ }
2838
+ );
2839
+ } else {
2840
+ recorded = true;
2841
+ }
2817
2842
  } catch (e) {
2818
2843
  this.report(e, `recording ${name}`);
2819
2844
  }
2820
- const begun = { ...run };
2821
2845
  const started = recorded ? this.updateState(name, (before) => ({ state: holdRun(onRunStart(definition, begun, before, this.now()), run.id), result: void 0 })).then(() => {
2822
2846
  }, (e) => this.report(e, `starting ${name}`)) : Promise.resolve();
2823
2847
  const recorder = createRecorder(run);
@@ -2840,11 +2864,23 @@ var Cronwatch = class {
2840
2864
  run.output = recorder.output() ?? (typeof result === "string" ? result : null);
2841
2865
  this.conclude(definition, run, result, error, threw, recorder.expectText() ?? (typeof result === "string" ? result : null));
2842
2866
  await started;
2867
+ const ignore = (why) => {
2868
+ if (why) this.report(new Error(`run ${run.id} of ${name} ${why}; ignored`), `finishing ${name}`);
2869
+ };
2870
+ const over = recorded || landedLate;
2843
2871
  try {
2844
- const ignored = await this.recordFinish(definition, run, recorded, finishedAt);
2845
- if (ignored) this.report(new Error(`run ${run.id} of ${name} ${ignored}; ignored`), `finishing ${name}`);
2872
+ ignore(await this.recordFinish(definition, run, over, finishedAt));
2846
2873
  } catch (e) {
2847
2874
  this.report(e, `recording ${name}`);
2875
+ if (!over && landedLate) {
2876
+ try {
2877
+ ignore(await this.recordFinish(definition, run, true, finishedAt));
2878
+ } catch (again) {
2879
+ this.report(again, `recording ${name}`);
2880
+ }
2881
+ }
2882
+ } finally {
2883
+ concluded = true;
2848
2884
  }
2849
2885
  return { run, result, error, threw };
2850
2886
  }
@@ -2881,7 +2917,7 @@ var Cronwatch = class {
2881
2917
  */
2882
2918
  async recordFinish(definition, run, recorded, finishedAt) {
2883
2919
  if (!recorded) {
2884
- await this.sync(definition);
2920
+ await this.sync(definition, false, true);
2885
2921
  try {
2886
2922
  await this.store.insertRun(run);
2887
2923
  await this.finishRun(toStored(definition), run, finishedAt);
@@ -2937,7 +2973,7 @@ ${id}`;
2937
2973
  }
2938
2974
  /**
2939
2975
  * The start of execute() without the function: the run is inserted and
2940
- * missed and stuck close (onRunStart). A store that fails is reported and
2976
+ * missed closes (onRunStart). A store that fails is reported and
2941
2977
  * the handle inserts the finished run instead, as execute() does.
2942
2978
  */
2943
2979
  async recordStart(definition, trigger = "start", id) {
@@ -3015,6 +3051,21 @@ ${id}`;
3015
3051
  let finished = inactive !== null;
3016
3052
  let finishCalled = false;
3017
3053
  let head = null;
3054
+ let flushed = null;
3055
+ const appendRedacted = (stored, lines) => {
3056
+ const added = stripNul(lines);
3057
+ const joins = flushed !== null && stored !== null && stored.endsWith(flushed.redacted);
3058
+ let raw = joins ? `${flushed.raw}
3059
+ ${added}` : added;
3060
+ const redacted = redactAndCap(raw, self.redact);
3061
+ const output = joins ? stored.slice(0, stored.length - flushed.redacted.length) + redacted : joinLines(stored, redacted);
3062
+ if (raw.length <= OUTPUT_CAP) return { output, tail: { raw, redacted } };
3063
+ const cut = raw.indexOf("\n", raw.length - OUTPUT_CAP);
3064
+ if (cut === -1) return { output, tail: null };
3065
+ raw = raw.slice(cut + 1);
3066
+ const kept = redactAndCap(raw, self.redact);
3067
+ return { output, tail: redacted.endsWith(kept) ? { raw, redacted: kept } : null };
3068
+ };
3018
3069
  let queue = Promise.resolve();
3019
3070
  const inTurn = (fn) => {
3020
3071
  const result = queue.then(fn);
@@ -3074,7 +3125,7 @@ ${id}`;
3074
3125
  durationMs: runDuration(from.startedAt, finishedAt),
3075
3126
  error: null,
3076
3127
  // Capped by conclude(), after it is redacted.
3077
- output: joinLines(from.output, added),
3128
+ output: added === null ? from.output : appendRedacted(from.output, added).output,
3078
3129
  metrics: { ...from.metrics, ...recorder.metrics() }
3079
3130
  };
3080
3131
  const expectText = joinLines(head, joinLines(from.output, recorder.expectText() ?? (typeof result === "string" ? result : null)));
@@ -3123,8 +3174,10 @@ ${id}`;
3123
3174
  self.report(new Error(`run ${id} of ${name} belongs to job "${stored.job}"; ignored`), `flushing ${name}`);
3124
3175
  return;
3125
3176
  }
3126
- const output = lines === null ? stored.output : joinOutput(stored.output, redactAndCap(lines, self.redact));
3177
+ const next = lines === null ? null : appendRedacted(stored.output, lines);
3178
+ const output = next === null ? stored.output : next.output === null ? null : capOutput(next.output);
3127
3179
  if (!await self.writeRunIf({ ...stored, output, metrics: { ...stored.metrics, ...metrics } }, ["running"])) return putBack();
3180
+ if (next !== null) flushed = next.tail !== null && output !== null && output.endsWith(next.tail.redacted) ? next.tail : null;
3128
3181
  const text = taken.expectText();
3129
3182
  if (text !== null && (head === null || head.length < OUTPUT_CAP)) head = (joinLines(head, text) ?? "").slice(0, OUTPUT_CAP);
3130
3183
  } catch (e) {
@@ -3224,7 +3277,7 @@ ${id}`;
3224
3277
  return [];
3225
3278
  }
3226
3279
  if (!evaluate) {
3227
- await this.updateState(run.job, (current) => ({ state: releaseRun(current, run.id), result: void 0 })).catch((e) => this.onError(e, `recording ${run.job}`));
3280
+ await this.updateState(run.job, (current) => ({ state: releaseRun(current, run.id), result: void 0 })).catch((e) => this.report(e, `recording ${run.job}`));
3228
3281
  return [];
3229
3282
  }
3230
3283
  if (claim.lateAfterTimeout && run.status !== "ok") return [];
@@ -3304,7 +3357,7 @@ ${id}`;
3304
3357
  try {
3305
3358
  alerts.push(...await source.sync(this) ?? []);
3306
3359
  } catch (e) {
3307
- this.onError(e, `source ${source.name}`);
3360
+ this.report(e, `source ${source.name}`);
3308
3361
  }
3309
3362
  }
3310
3363
  for (const definition of this.definitions.values()) await this.sync(definition);
@@ -3321,10 +3374,13 @@ ${id}`;
3321
3374
  const run = await this.store.getRun(listed.id);
3322
3375
  if (!run || run.status !== "running" || run.job !== listed.job) continue;
3323
3376
  run.status = "timeout";
3324
- if (stranded) {
3377
+ if (stranded && run.error !== null) {
3378
+ run.status = "failed";
3379
+ if (run.durationMs !== null) run.finishedAt = run.startedAt + run.durationMs;
3380
+ else run.durationMs = runDuration(run.startedAt, run.finishedAt);
3381
+ } else if (stranded) {
3325
3382
  run.durationMs ??= runDuration(run.startedAt, run.finishedAt);
3326
- run.error = STRANDED_ERROR + (run.error === null ? "" : `
3327
- ${run.error}`);
3383
+ run.error = STRANDED_ERROR;
3328
3384
  } else {
3329
3385
  run.finishedAt = now;
3330
3386
  run.durationMs = runDuration(run.startedAt, now);
@@ -3333,7 +3389,7 @@ ${run.error}`);
3333
3389
  if (!await this.writeRunIf(run, ["running"])) continue;
3334
3390
  alerts.push(...await this.finishRun(definition, run, now));
3335
3391
  } catch (e) {
3336
- this.onError(e, `checking ${listed.job}`);
3392
+ this.report(e, `checking ${listed.job}`);
3337
3393
  }
3338
3394
  }
3339
3395
  const jobs = [];
@@ -3357,7 +3413,7 @@ ${run.error}`);
3357
3413
  alerts.push(...await this.evaluatePending(stored, recent, state, now));
3358
3414
  jobs.push(summarize(stored, recent, state, nextExpectedAt, now));
3359
3415
  } catch (e) {
3360
- this.onError(e, `checking ${stored.name}`);
3416
+ this.report(e, `checking ${stored.name}`);
3361
3417
  jobs.push(await this.unevaluable(stored, now));
3362
3418
  }
3363
3419
  }
@@ -3367,7 +3423,7 @@ ${run.error}`);
3367
3423
  try {
3368
3424
  pruned = await this.store.prune(now - this.retentionMs);
3369
3425
  } catch (e) {
3370
- this.onError(e, "pruning");
3426
+ this.report(e, "pruning");
3371
3427
  }
3372
3428
  }
3373
3429
  return { checkedAt: now, jobs, alerts, pruned };
@@ -3393,7 +3449,7 @@ ${run.error}`);
3393
3449
  if (run.status === "running" || typeof run.finishedAt !== "number" || run.finishedAt > now - SEND_LEASE_MS) continue;
3394
3450
  alerts.push(...await this.finishRun(stored.definition, run, now, true));
3395
3451
  } catch (e) {
3396
- this.onError(e, `evaluating run ${id} of ${stored.name}`);
3452
+ this.report(e, `evaluating run ${id} of ${stored.name}`);
3397
3453
  }
3398
3454
  }
3399
3455
  return alerts;
@@ -3408,7 +3464,7 @@ ${run.error}`);
3408
3464
  const { nextExpectedAt } = onCheck(stored.definition, stored, recent[0] ?? null, state, now);
3409
3465
  return { job: summarize(stored, recent, state, nextExpectedAt, now), runs: recent.slice(0, runs) };
3410
3466
  } catch (e) {
3411
- this.onError(e, `reading ${stored.name}`);
3467
+ this.report(e, `reading ${stored.name}`);
3412
3468
  return { job: await this.unevaluable(stored, now), runs: recent.slice(0, runs) };
3413
3469
  }
3414
3470
  }
@@ -3435,7 +3491,7 @@ ${run.error}`);
3435
3491
  async jobSummary(name) {
3436
3492
  await this.ensureReady();
3437
3493
  const definition = this.definitions.get(name);
3438
- if (definition) await this.sync(definition, true);
3494
+ if (definition) await this.sync(definition, true, false);
3439
3495
  const stored = await this.store.getJob(name);
3440
3496
  if (!stored) return null;
3441
3497
  return (await this.snapshot(this.readJob(stored), this.now(), 0)).job;
@@ -3694,10 +3750,6 @@ function joinLines(before, after2) {
3694
3750
  return `${before}
3695
3751
  ${after2}`;
3696
3752
  }
3697
- function joinOutput(before, after2) {
3698
- const joined = joinLines(before, after2);
3699
- return joined === null ? null : capOutput(joined);
3700
- }
3701
3753
  function clampLimit(limit, fallback, min) {
3702
3754
  const n = typeof limit === "number" && Number.isFinite(limit) ? Math.trunc(limit) : fallback;
3703
3755
  return Math.min(500, Math.max(min, n));
@@ -3712,6 +3764,20 @@ function after(ms, fn) {
3712
3764
  arm(ms);
3713
3765
  return () => clearTimeout(timer);
3714
3766
  }
3767
+ var TIMED_OUT = /* @__PURE__ */ Symbol("timed out");
3768
+ function within(promise, ms) {
3769
+ return new Promise((resolve, reject) => {
3770
+ const t = setTimeout(() => resolve(TIMED_OUT), ms);
3771
+ if (typeof t === "object" && "unref" in t) t.unref();
3772
+ promise.then((v) => {
3773
+ clearTimeout(t);
3774
+ resolve(v);
3775
+ }, (e) => {
3776
+ clearTimeout(t);
3777
+ reject(e);
3778
+ });
3779
+ });
3780
+ }
3715
3781
  function withTimeout(promise, ms) {
3716
3782
  return new Promise((resolve, reject) => {
3717
3783
  const t = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);