@aglyn/aglyn 1.0.0-beta.243 → 1.0.0-beta.246

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.
@@ -537,6 +537,45 @@ export function signupDroughtHealth(markers, attemptMarkers, orgCreations, ms, m
537
537
  minimumTraffic
538
538
  });
539
539
  }
540
+ /** See {@link SignupLandingCheck}: twelve hours of orgs. */ export const SIGNUP_LANDING_WINDOW_HOURS = 12;
541
+ /**
542
+ * Thirty minutes before an org with no site counts. Sign-ups that do make
543
+ * a site make it within minutes of the workspace (38 in the backtest month:
544
+ * median under a minute, slowest 8.2), so thirty never counts a person still
545
+ * typing a site name.
546
+ */ export const SIGNUP_LANDING_GRACE_MINUTES = 30;
547
+ /** Red at two stranded orgs in the window. See {@link SignupLandingCheck}. */ export const MAX_STRANDED_SIGNUPS = 2;
548
+ export function signupLandingHealth(orgs, ms, maxStranded = MAX_STRANDED_SIGNUPS, windowHours = SIGNUP_LANDING_WINDOW_HOURS, graceMinutes = SIGNUP_LANDING_GRACE_MINUTES) {
549
+ const shape = {
550
+ windowHours,
551
+ graceMinutes,
552
+ maxStranded
553
+ };
554
+ // An unreadable window is degraded, never calm, the rule every sibling
555
+ // follows: a check that cannot see the thing it watches has no opinion.
556
+ if (orgs === null) {
557
+ return _extends({
558
+ ok: false,
559
+ ms,
560
+ code: 'orgs-unavailable',
561
+ strandedOrgs: null,
562
+ orgsInWindow: null
563
+ }, shape);
564
+ }
565
+ // `!(n > 0)` rather than `n === 0`, so a corrupt count (NaN, a string)
566
+ // reads as "no site" and cannot hide a stranded org.
567
+ const strandedOrgs = orgs.filter((landing)=>!(Number(landing.hostCount) > 0)).length;
568
+ const stranded = strandedOrgs >= maxStranded;
569
+ return _extends({
570
+ ok: !stranded,
571
+ ms
572
+ }, stranded ? {
573
+ code: 'signups-stranded'
574
+ } : {}, {
575
+ strandedOrgs,
576
+ orgsInWindow: orgs.length
577
+ }, shape);
578
+ }
540
579
  /**
541
580
  * Trailing window the degradation counts cover.
542
581
  *
@@ -1819,7 +1858,30 @@ function parseCron(expression) {
1819
1858
  }
1820
1859
  return checks;
1821
1860
  }
1861
+ /**
1862
+ * How often one instance re-marks an every-minute job.
1863
+ *
1864
+ * A job that runs every minute and is graded against a thirty-minute grace
1865
+ * does not need sixty marks an hour: each mark is two writes, and the two
1866
+ * every-minute beats alone were 5,760 writes a day. One mark per instance per
1867
+ * five minutes leaves a silent job silent for at most five minutes plus the
1868
+ * grace before it reads `job-silent` — still inside the budget the grace
1869
+ * describes, and an instance that stops running stops marking either way.
1870
+ */ export const CRON_BEAT_EVERY_MINUTE_MIN_INTERVAL_MS = 5 * 60000;
1871
+ /** The jobs whose marks {@link CRON_BEAT_EVERY_MINUTE_MIN_INTERVAL_MS} spaces out. */ const EVERY_MINUTE_CRON_JOBS = new Set(SCHEDULED_JOBS.filter((job)=>job.cron === '* * * * *').map((job)=>job.id));
1872
+ /** When this instance last marked each every-minute job. */ const lastEveryMinuteBeat = new Map();
1873
+ /** Test seam. */ export function resetCronBeatThrottleForTests() {
1874
+ lastEveryMinuteBeat.clear();
1875
+ }
1822
1876
  export async function writeCronBeat(store, jobId, now = Date.now()) {
1877
+ const throttled = EVERY_MINUTE_CRON_JOBS.has(jobId);
1878
+ if (throttled) {
1879
+ const last = lastEveryMinuteBeat.get(jobId);
1880
+ // `>= 0` so a clock that steps backwards marks rather than going quiet.
1881
+ if (last !== undefined && now - last >= 0 && now - last < CRON_BEAT_EVERY_MINUTE_MIN_INTERVAL_MS) {
1882
+ return true;
1883
+ }
1884
+ }
1823
1885
  try {
1824
1886
  const beats = store.collection(CRON_BEAT_COLLECTION);
1825
1887
  await beats.doc(jobId).set({
@@ -1840,6 +1902,7 @@ export async function writeCronBeat(store, jobId, now = Date.now()) {
1840
1902
  } catch (unused) {
1841
1903
  // The reader confirms a stale summary entry against the mark above.
1842
1904
  }
1905
+ if (throttled) lastEveryMinuteBeat.set(jobId, now);
1843
1906
  return true;
1844
1907
  } catch (unused) {
1845
1908
  return false;