sermon-js 0.0.0-stage → 0.2.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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -2
  3. package/dist/api/index.cjs +174 -0
  4. package/dist/api/index.cjs.map +1 -0
  5. package/dist/api/index.d.cts +77 -0
  6. package/dist/api/index.d.ts +77 -0
  7. package/dist/api/index.js +171 -0
  8. package/dist/api/index.js.map +1 -0
  9. package/dist/chunk-2M4UBCDM.js +178 -0
  10. package/dist/chunk-2M4UBCDM.js.map +1 -0
  11. package/dist/chunk-AH372HLN.js +98 -0
  12. package/dist/chunk-AH372HLN.js.map +1 -0
  13. package/dist/chunk-APC2HVSI.js +154 -0
  14. package/dist/chunk-APC2HVSI.js.map +1 -0
  15. package/dist/chunk-D7BBJTYU.cjs +335 -0
  16. package/dist/chunk-D7BBJTYU.cjs.map +1 -0
  17. package/dist/chunk-ELV4ZU6G.cjs +51 -0
  18. package/dist/chunk-ELV4ZU6G.cjs.map +1 -0
  19. package/dist/chunk-EMP7XVKC.js +334 -0
  20. package/dist/chunk-EMP7XVKC.js.map +1 -0
  21. package/dist/chunk-I3346MUL.js +49 -0
  22. package/dist/chunk-I3346MUL.js.map +1 -0
  23. package/dist/chunk-NP3YYVRZ.cjs +163 -0
  24. package/dist/chunk-NP3YYVRZ.cjs.map +1 -0
  25. package/dist/chunk-PARE6EUZ.cjs +184 -0
  26. package/dist/chunk-PARE6EUZ.cjs.map +1 -0
  27. package/dist/chunk-XHXEKB3Q.cjs +241 -0
  28. package/dist/chunk-XHXEKB3Q.cjs.map +1 -0
  29. package/dist/chunk-XPXIDJUU.js +229 -0
  30. package/dist/chunk-XPXIDJUU.js.map +1 -0
  31. package/dist/chunk-ZI442ZA5.cjs +100 -0
  32. package/dist/chunk-ZI442ZA5.cjs.map +1 -0
  33. package/dist/config-BEbP-uLf.d.cts +125 -0
  34. package/dist/config-w1tm1bdu.d.ts +125 -0
  35. package/dist/generic-BJmlgtUM.d.ts +31 -0
  36. package/dist/generic-lmrGxMDC.d.cts +31 -0
  37. package/dist/index.cjs +407 -0
  38. package/dist/index.cjs.map +1 -0
  39. package/dist/index.d.cts +93 -0
  40. package/dist/index.d.ts +93 -0
  41. package/dist/index.js +330 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/presets/index.cjs +1012 -0
  44. package/dist/presets/index.cjs.map +1 -0
  45. package/dist/presets/index.d.cts +516 -0
  46. package/dist/presets/index.d.ts +516 -0
  47. package/dist/presets/index.js +971 -0
  48. package/dist/presets/index.js.map +1 -0
  49. package/dist/react/heroui/index.cjs +822 -0
  50. package/dist/react/heroui/index.cjs.map +1 -0
  51. package/dist/react/heroui/index.d.cts +122 -0
  52. package/dist/react/heroui/index.d.ts +122 -0
  53. package/dist/react/heroui/index.js +821 -0
  54. package/dist/react/heroui/index.js.map +1 -0
  55. package/dist/react/heroui/styles.css +11 -0
  56. package/dist/react/index.cjs +71 -0
  57. package/dist/react/index.cjs.map +1 -0
  58. package/dist/react/index.d.cts +53 -0
  59. package/dist/react/index.d.ts +53 -0
  60. package/dist/react/index.js +70 -0
  61. package/dist/react/index.js.map +1 -0
  62. package/dist/status-DsWzgb9Q.d.cts +146 -0
  63. package/dist/status-zd04ZQ98.d.ts +146 -0
  64. package/dist/store-DybhDphS.d.cts +77 -0
  65. package/dist/store-_GuzSP0z.d.ts +77 -0
  66. package/dist/stores/mongodb.cjs +99 -0
  67. package/dist/stores/mongodb.cjs.map +1 -0
  68. package/dist/stores/mongodb.d.cts +40 -0
  69. package/dist/stores/mongodb.d.ts +40 -0
  70. package/dist/stores/mongodb.js +97 -0
  71. package/dist/stores/mongodb.js.map +1 -0
  72. package/dist/stores/redis.cjs +23 -0
  73. package/dist/stores/redis.cjs.map +1 -0
  74. package/dist/stores/redis.d.cts +31 -0
  75. package/dist/stores/redis.d.ts +31 -0
  76. package/dist/stores/redis.js +21 -0
  77. package/dist/stores/redis.js.map +1 -0
  78. package/dist/stores/sql.cjs +164 -0
  79. package/dist/stores/sql.cjs.map +1 -0
  80. package/dist/stores/sql.d.cts +39 -0
  81. package/dist/stores/sql.d.ts +39 -0
  82. package/dist/stores/sql.js +162 -0
  83. package/dist/stores/sql.js.map +1 -0
  84. package/dist/stores/upstash.cjs +27 -0
  85. package/dist/stores/upstash.cjs.map +1 -0
  86. package/dist/stores/upstash.d.cts +19 -0
  87. package/dist/stores/upstash.d.ts +19 -0
  88. package/dist/stores/upstash.js +25 -0
  89. package/dist/stores/upstash.js.map +1 -0
  90. package/dist/testing/index.cjs +209 -0
  91. package/dist/testing/index.cjs.map +1 -0
  92. package/dist/testing/index.d.cts +66 -0
  93. package/dist/testing/index.d.ts +66 -0
  94. package/dist/testing/index.js +205 -0
  95. package/dist/testing/index.js.map +1 -0
  96. package/dist/tree-BXbXfjF1.d.cts +236 -0
  97. package/dist/tree-BXbXfjF1.d.ts +236 -0
  98. package/dist/types-CctvUAs2.d.cts +192 -0
  99. package/dist/types-CctvUAs2.d.ts +192 -0
  100. package/package.json +233 -3
@@ -0,0 +1,125 @@
1
+ import { l as SecretsInput, f as CheckContext, d as CheckReturn, h as CommonOptions } from './types-CctvUAs2.js';
2
+
3
+ declare const DEFAULT_SLOW_MS = 2000;
4
+ declare const DEFAULT_TIMEOUT_MS = 10000;
5
+ type AnyCheck = (ctx: CheckContext<SecretsInput>) => Promise<CheckReturn> | CheckReturn;
6
+ /** A service ready to be checked. Built by `custom()` or a preset; not meant to be written by hand. */
7
+ type ServiceDefinition = {
8
+ node: "service";
9
+ /** The preset kind: "custom", "generic.http", "fly.app"… */
10
+ kind: string;
11
+ name: string;
12
+ description?: string;
13
+ critical: boolean;
14
+ slowMs: number;
15
+ timeoutMs: number;
16
+ /** Natural identity (e.g. `fly.app:my-app`): keeps history across renames and moves */
17
+ identity?: string;
18
+ /**
19
+ * Hidden from `console.log`, `JSON.stringify` and object spreads (the property is not
20
+ * enumerable), so a logged or serialized configuration never prints credentials.
21
+ */
22
+ secrets: SecretsInput;
23
+ /** Other values to redact from public text (e.g. credentials given as headers). Hidden like `secrets`. */
24
+ sensitive?: string[];
25
+ /** Required secrets/fields that are empty: the service is shown as "not configured" and never runs */
26
+ missing: string[];
27
+ check: AnyCheck;
28
+ };
29
+ type GroupDefinition = {
30
+ node: "group";
31
+ title: string;
32
+ description?: string;
33
+ services: ServiceNode[];
34
+ };
35
+ type ServiceNode = ServiceDefinition | GroupDefinition;
36
+ /** A titled set of services. Groups can contain other groups. */
37
+ declare function group(options: {
38
+ title: string;
39
+ description?: string;
40
+ services: ServiceNode[];
41
+ }): GroupDefinition;
42
+ type CustomOptions<Secrets extends SecretsInput> = CommonOptions & {
43
+ /** How to call it (required for custom services) */
44
+ name: string;
45
+ /** Values the check needs. They're redacted from public errors; if one is empty the service is "not configured". */
46
+ secrets?: Secrets;
47
+ /** Set to false to list the service as "not configured" without running it */
48
+ enabled?: boolean;
49
+ /** Return `true`/nothing for up, `false` for down, or a full outcome. Throwing means down. */
50
+ check: (ctx: CheckContext<Secrets>) => Promise<CheckReturn> | CheckReturn;
51
+ };
52
+ /** Any check you can write yourself: the escape hatch for services without a preset */
53
+ declare function custom<Secrets extends SecretsInput = Record<never, never>>(options: CustomOptions<Secrets>): ServiceDefinition;
54
+ /**
55
+ * What every preset accepts on top of its own fields. Declare a preset's options as
56
+ * `PresetOptions & { …fields; secrets: { … }; track?: { … } }`.
57
+ */
58
+ type PresetOptions = CommonOptions;
59
+ /** The shape a preset's options must fit (kept separate so `secrets` and `track` stay exact per preset) */
60
+ type PresetOptionsShape = CommonOptions & {
61
+ secrets?: SecretsInput;
62
+ track?: unknown;
63
+ };
64
+ type SecretsOf<O> = O extends {
65
+ secrets?: infer S;
66
+ } ? (NonNullable<S> extends SecretsInput ? NonNullable<S> : never) : never;
67
+ /** Inside a preset's check: required secrets are strings, optional ones may be missing */
68
+ type PresetContext<O extends PresetOptionsShape, Required extends string> = Omit<CheckContext<SecretsInput>, "secrets"> & {
69
+ secrets: {
70
+ [K in Extract<keyof SecretsOf<O>, string>]: K extends Required ? string : string | undefined;
71
+ };
72
+ /** The options the user passed to the preset */
73
+ options: O;
74
+ };
75
+ type PresetSpec<O extends PresetOptionsShape, Required extends string> = {
76
+ /** Namespaced kind, e.g. "fly.app" */
77
+ kind: string;
78
+ /** Secrets that must be non-empty for the service to run; "all" requires every secret given */
79
+ requiredSecrets?: readonly Required[] | "all";
80
+ /** Options that must be non-empty at runtime (e.g. a URL read from the environment) */
81
+ requiredFields?: readonly (keyof O & string)[];
82
+ defaults?: {
83
+ slowMs?: number;
84
+ timeoutMs?: number;
85
+ };
86
+ /** Name, natural identity and description derived from the options */
87
+ derive: (options: O) => {
88
+ name: string;
89
+ identity?: string;
90
+ description?: string;
91
+ };
92
+ /** Values outside `secrets` that must never show in public text (e.g. credentials in headers) */
93
+ sensitive?: (options: O) => Array<string | undefined>;
94
+ check: (ctx: PresetContext<O, NoInfer<Required>>) => Promise<CheckReturn> | CheckReturn;
95
+ };
96
+ /**
97
+ * Build a preset: a typed function that turns a few options into a service.
98
+ *
99
+ * ```ts
100
+ * type Options = PresetOptions & { app: string; secrets: { token: string | undefined } };
101
+ * export const app = definePreset<Options>()({
102
+ * kind: "fly.app",
103
+ * requiredSecrets: ["token"],
104
+ * derive: (o) => ({ name: o.name ?? o.app, identity: `fly.app:${o.app}` }),
105
+ * check: ({ options, secrets, http }) => …,
106
+ * });
107
+ * ```
108
+ */
109
+ declare function definePreset<O extends PresetOptionsShape>(): <const Required extends keyof SecretsOf<O> & string = never>(spec: PresetSpec<O, Required>) => (options: O) => ServiceDefinition;
110
+ /** A service with its derived key */
111
+ type ResolvedService = ServiceDefinition & {
112
+ key: string;
113
+ };
114
+ type ResolvedNode = {
115
+ type: "group";
116
+ key: string;
117
+ title: string;
118
+ description?: string;
119
+ children: ResolvedNode[];
120
+ } | {
121
+ type: "service";
122
+ key: string;
123
+ };
124
+
125
+ export { type CustomOptions as C, DEFAULT_SLOW_MS as D, type GroupDefinition as G, type PresetContext as P, type ResolvedNode as R, type ServiceDefinition as S, DEFAULT_TIMEOUT_MS as a, type PresetOptions as b, type PresetSpec as c, type ResolvedService as d, type ServiceNode as e, custom as f, definePreset as g, group as h };
@@ -0,0 +1,31 @@
1
+ import { b as PresetOptions, S as ServiceDefinition } from './config-w1tm1bdu.js';
2
+ import { l as SecretsInput, c as HttpResult, d as CheckReturn } from './types-CctvUAs2.js';
3
+
4
+ type GenericHttpOptions<Secrets extends SecretsInput = SecretsInput> = PresetOptions & {
5
+ /** How to call it */
6
+ name: string;
7
+ /** The URL to call. When empty (e.g. an unset environment variable) the service is "not configured". */
8
+ url: string | undefined;
9
+ method?: string;
10
+ /** Static headers, or a function of the secrets (e.g. to build an Authorization header) */
11
+ headers?: Record<string, string> | ((secrets: {
12
+ [K in keyof Secrets]-?: string;
13
+ }) => Record<string, string>);
14
+ body?: string;
15
+ /** Every secret given is required and redacted from public errors */
16
+ secrets?: Secrets;
17
+ track?: {
18
+ /** Statuses that mean "up" (default: any 2xx/3xx) */
19
+ status?: number[];
20
+ /** Inspect the JSON body of a healthy answer to refine the outcome (metrics, usage, degraded…) */
21
+ json?: (body: unknown, response: HttpResult) => CheckReturn;
22
+ };
23
+ };
24
+ declare const preset: (options: GenericHttpOptions<SecretsInput>) => ServiceDefinition;
25
+ /** Presets that work with any service */
26
+ declare const generic: {
27
+ /** Any HTTP endpoint: up on 2xx/3xx (or `track.status`), degraded on 429 or when slow, down otherwise */
28
+ http: <Secrets extends SecretsInput = Record<never, never>>(options: GenericHttpOptions<Secrets>) => ReturnType<typeof preset>;
29
+ };
30
+
31
+ export { type GenericHttpOptions as G, generic as g };
@@ -0,0 +1,31 @@
1
+ import { b as PresetOptions, S as ServiceDefinition } from './config-BEbP-uLf.cjs';
2
+ import { l as SecretsInput, c as HttpResult, d as CheckReturn } from './types-CctvUAs2.cjs';
3
+
4
+ type GenericHttpOptions<Secrets extends SecretsInput = SecretsInput> = PresetOptions & {
5
+ /** How to call it */
6
+ name: string;
7
+ /** The URL to call. When empty (e.g. an unset environment variable) the service is "not configured". */
8
+ url: string | undefined;
9
+ method?: string;
10
+ /** Static headers, or a function of the secrets (e.g. to build an Authorization header) */
11
+ headers?: Record<string, string> | ((secrets: {
12
+ [K in keyof Secrets]-?: string;
13
+ }) => Record<string, string>);
14
+ body?: string;
15
+ /** Every secret given is required and redacted from public errors */
16
+ secrets?: Secrets;
17
+ track?: {
18
+ /** Statuses that mean "up" (default: any 2xx/3xx) */
19
+ status?: number[];
20
+ /** Inspect the JSON body of a healthy answer to refine the outcome (metrics, usage, degraded…) */
21
+ json?: (body: unknown, response: HttpResult) => CheckReturn;
22
+ };
23
+ };
24
+ declare const preset: (options: GenericHttpOptions<SecretsInput>) => ServiceDefinition;
25
+ /** Presets that work with any service */
26
+ declare const generic: {
27
+ /** Any HTTP endpoint: up on 2xx/3xx (or `track.status`), degraded on 429 or when slow, down otherwise */
28
+ http: <Secrets extends SecretsInput = Record<never, never>>(options: GenericHttpOptions<Secrets>) => ReturnType<typeof preset>;
29
+ };
30
+
31
+ export { type GenericHttpOptions as G, generic as g };
package/dist/index.cjs ADDED
@@ -0,0 +1,407 @@
1
+ 'use strict';
2
+
3
+ var chunkNP3YYVRZ_cjs = require('./chunk-NP3YYVRZ.cjs');
4
+ var chunkELV4ZU6G_cjs = require('./chunk-ELV4ZU6G.cjs');
5
+ var chunkPARE6EUZ_cjs = require('./chunk-PARE6EUZ.cjs');
6
+ var chunkXHXEKB3Q_cjs = require('./chunk-XHXEKB3Q.cjs');
7
+
8
+ // src/stores/memory.ts
9
+ function memoryStore({ now = () => Date.now() } = {}) {
10
+ let latest = {};
11
+ let runs = [];
12
+ const samples = /* @__PURE__ */ new Map();
13
+ const locks = /* @__PURE__ */ new Map();
14
+ const cache = /* @__PURE__ */ new Map();
15
+ return {
16
+ async saveRun(run) {
17
+ for (const result of run.results) latest[result.key] = result;
18
+ for (const sample of run.samples) {
19
+ const list = samples.get(sample.key) ?? [];
20
+ list.push(sample);
21
+ samples.set(sample.key, list);
22
+ }
23
+ runs.push(run.summary);
24
+ },
25
+ async latest() {
26
+ return { ...latest };
27
+ },
28
+ async runs({ since, until }) {
29
+ return runs.filter((run) => run.at >= since && run.at <= until).sort((a, b) => a.at - b.at);
30
+ },
31
+ async samples(key, { since, until }) {
32
+ return (samples.get(key) ?? []).filter((sample) => sample.at >= since && sample.at <= until).sort((a, b) => a.at - b.at);
33
+ },
34
+ async prune({ before, keepKeys }) {
35
+ runs = runs.filter((run) => run.at >= before);
36
+ for (const [key, list] of samples) {
37
+ const kept = list.filter((sample) => sample.at >= before);
38
+ if (kept.length) samples.set(key, kept);
39
+ else samples.delete(key);
40
+ }
41
+ if (keepKeys) {
42
+ const keep = new Set(keepKeys);
43
+ latest = Object.fromEntries(Object.entries(latest).filter(([key]) => keep.has(key)));
44
+ for (const key of [...samples.keys()]) if (!keep.has(key)) samples.delete(key);
45
+ }
46
+ },
47
+ async lock(name, ttlMs) {
48
+ const current = now();
49
+ const expiresAt = locks.get(name);
50
+ if (expiresAt !== void 0 && expiresAt > current) {
51
+ return { ok: false, retryAfterMs: expiresAt - current };
52
+ }
53
+ locks.set(name, current + ttlMs);
54
+ return { ok: true };
55
+ },
56
+ async unlock(name) {
57
+ locks.delete(name);
58
+ },
59
+ cache: {
60
+ async get(key) {
61
+ const entry = cache.get(key);
62
+ if (!entry) return null;
63
+ if (entry.expiresAt <= now()) {
64
+ cache.delete(key);
65
+ return null;
66
+ }
67
+ return entry.value;
68
+ },
69
+ async set(key, value, ttlMs) {
70
+ cache.set(key, { value, expiresAt: now() + ttlMs });
71
+ }
72
+ }
73
+ };
74
+ }
75
+
76
+ // src/status.ts
77
+ var TICK_LOCK = "sermon:tick";
78
+ var STATS_KEY = "sermon:stats";
79
+ var LAST_RUN_KEY = "sermon:last-full-run";
80
+ function sampleOf(result) {
81
+ const healthy = result.status === "up" || result.status === "sleeping";
82
+ return {
83
+ key: result.key,
84
+ at: result.checkedAt,
85
+ status: result.status,
86
+ latencyMs: result.latencyMs,
87
+ httpStatus: result.httpStatus,
88
+ error: healthy ? void 0 : result.error,
89
+ instances: result.instances?.length ? Object.fromEntries(
90
+ result.instances.map((instance) => [instance.key, [instance.status, instance.latencyMs]])
91
+ ) : void 0
92
+ };
93
+ }
94
+ function parseJson(value) {
95
+ if (!value) return void 0;
96
+ try {
97
+ return JSON.parse(value);
98
+ } catch {
99
+ return void 0;
100
+ }
101
+ }
102
+ function defineStatus(config) {
103
+ const now = config.now ?? (() => Date.now());
104
+ const resolved = chunkXHXEKB3Q_cjs.resolveTree(config.services ?? []);
105
+ const { services, tree } = resolved;
106
+ const store = config.store ?? memoryStore({ now });
107
+ const intervalMs = chunkNP3YYVRZ_cjs.toMs(config.interval ?? "5m");
108
+ const retentionMs = chunkNP3YYVRZ_cjs.toMs(config.retention ?? "7d");
109
+ const scoring = chunkNP3YYVRZ_cjs.resolveScoring(config.scoring);
110
+ const warn = config.onWarning ?? ((message) => console.warn(`[sermon-js] ${message}`));
111
+ for (const warning of resolved.warnings) warn(warning);
112
+ const secretValues = chunkPARE6EUZ_cjs.collectSecretValues(
113
+ [...services.values()].flatMap((service) => [...Object.values(service.secrets), ...service.sensitive ?? []])
114
+ );
115
+ const fallback = memoryStore({ now });
116
+ const cache = store.cache ?? fallback.cache;
117
+ const lock = (name, ttlMs) => store.lock ? store.lock(name, ttlMs) : fallback.lock(name, ttlMs);
118
+ const unlock = async (name) => {
119
+ if (store.lock) await store.unlock?.(name);
120
+ else await fallback.unlock(name);
121
+ };
122
+ const longestCheckMs = Math.max(0, ...[...services.values()].map((service) => service.timeoutMs));
123
+ const tickLockMs = longestCheckMs + 6e3;
124
+ const checkCache = (key) => ({
125
+ get: (name) => cache.get(`sermon:check:${key}:${name}`),
126
+ set: (name, value, ttlMs) => cache.set(`sermon:check:${key}:${name}`, value, ttlMs)
127
+ });
128
+ async function currentLatest() {
129
+ const stored = await store.latest();
130
+ const latest = {};
131
+ for (const [key, service] of services) {
132
+ const result = stored[key];
133
+ if (!result) continue;
134
+ if (service.missing.length > 0 && result.status !== "unconfigured") continue;
135
+ latest[key] = result;
136
+ }
137
+ return latest;
138
+ }
139
+ function scoreOf(latest) {
140
+ return chunkNP3YYVRZ_cjs.computeScore({
141
+ scoring,
142
+ results: [...services.values()].filter((service) => latest[service.key]).map((service) => ({ status: latest[service.key].status, critical: service.critical }))
143
+ });
144
+ }
145
+ async function readStats() {
146
+ return parseJson(await cache.get(STATS_KEY)) ?? {};
147
+ }
148
+ async function refreshStats(keys, known) {
149
+ const at = now();
150
+ const stats = known ?? await readStats();
151
+ await Promise.all(
152
+ keys.map(async (key) => {
153
+ const samples = await store.samples(key, { since: at - retentionMs, until: at });
154
+ stats[key] = chunkNP3YYVRZ_cjs.computeServiceStats({ samples, now: at });
155
+ })
156
+ );
157
+ for (const key of Object.keys(stats)) if (!services.has(key)) delete stats[key];
158
+ await cache.set(STATS_KEY, JSON.stringify(stats), retentionMs);
159
+ return stats;
160
+ }
161
+ async function lastFullRun() {
162
+ const cached = parseJson(await cache.get(LAST_RUN_KEY));
163
+ if (cached) return cached;
164
+ const at = now();
165
+ const runs = await store.runs({ since: at - retentionMs, until: at });
166
+ const last = [...runs].reverse().find((run) => run.full);
167
+ return last ? { at: last.at, trigger: last.trigger } : null;
168
+ }
169
+ const status = {
170
+ services,
171
+ tree,
172
+ store,
173
+ intervalMs,
174
+ retentionMs,
175
+ scoring,
176
+ warnings: resolved.warnings,
177
+ redact: (text) => chunkPARE6EUZ_cjs.redactText({ text, secretValues }),
178
+ has: (key) => services.has(key),
179
+ lock,
180
+ lastFullRun,
181
+ async run({ keys, trigger = "manual" } = {}) {
182
+ const unknown = (keys ?? []).filter((key) => !services.has(key));
183
+ if (unknown.length) throw new Error(`sermon-js: unknown service key(s): ${unknown.join(", ")}`);
184
+ const selected = keys ? keys.map((key) => services.get(key)) : [...services.values()];
185
+ const full = !keys;
186
+ const results = await chunkPARE6EUZ_cjs.runServices({
187
+ services: selected,
188
+ secretValues,
189
+ cacheFor: checkCache,
190
+ now,
191
+ fetch: config.fetch,
192
+ hideResponseBodies: config.hideResponseBodies
193
+ });
194
+ const latest = full ? {} : await currentLatest();
195
+ for (const result of results) latest[result.key] = result;
196
+ const { score, label } = scoreOf(latest);
197
+ const at = now();
198
+ const failing = {};
199
+ for (const result of results) {
200
+ if (result.status === "down" || result.status === "degraded") failing[result.key] = result.status;
201
+ }
202
+ const summary = { at, trigger, full, score, label, failing };
203
+ const samples = results.filter((result) => result.status !== "unconfigured").map(sampleOf);
204
+ await store.saveRun({ summary, results, samples });
205
+ if (full) {
206
+ await store.prune({ before: at - retentionMs, keepKeys: [...services.keys()] });
207
+ await cache.set(LAST_RUN_KEY, JSON.stringify({ at, trigger }), retentionMs);
208
+ }
209
+ await refreshStats(samples.map((sample) => sample.key));
210
+ return { at, trigger, full, score, label, results };
211
+ },
212
+ async tick({ force = false } = {}) {
213
+ const at = now();
214
+ const tolerance = Math.min(intervalMs * 0.1, 3e4);
215
+ const last = await lastFullRun();
216
+ if (!force && last && at - last.at < intervalMs - tolerance) {
217
+ return { ran: false, reason: "not-due", nextDueAt: last.at + intervalMs };
218
+ }
219
+ const held = await lock(TICK_LOCK, tickLockMs);
220
+ if (!held.ok) {
221
+ return { ran: false, reason: "locked", nextDueAt: at + held.retryAfterMs };
222
+ }
223
+ try {
224
+ const latest = force ? null : await lastFullRun();
225
+ if (latest && now() - latest.at < intervalMs - tolerance) {
226
+ return { ran: false, reason: "not-due", nextDueAt: latest.at + intervalMs };
227
+ }
228
+ return { ran: true, report: await status.run({ trigger: "scheduled" }) };
229
+ } finally {
230
+ await unlock(TICK_LOCK).catch(() => void 0);
231
+ }
232
+ },
233
+ async snapshot() {
234
+ const at = now();
235
+ const [latest, cachedStats, lastRun] = await Promise.all([currentLatest(), readStats(), lastFullRun()]);
236
+ const missing = Object.values(latest).filter((result) => result.status !== "unconfigured" && !cachedStats[result.key]).map((result) => result.key);
237
+ const stats = missing.length ? await refreshStats(missing, cachedStats) : cachedStats;
238
+ const toSnapshot = (node) => {
239
+ if (node.type === "group") {
240
+ return {
241
+ type: "group",
242
+ key: node.key,
243
+ title: node.title,
244
+ description: node.description,
245
+ children: node.children.map(toSnapshot)
246
+ };
247
+ }
248
+ const service = services.get(node.key);
249
+ const configured = service.missing.length === 0;
250
+ return {
251
+ type: "service",
252
+ key: service.key,
253
+ kind: service.kind,
254
+ name: service.name,
255
+ description: service.description,
256
+ critical: service.critical,
257
+ slowMs: service.slowMs,
258
+ configured,
259
+ missing: configured ? void 0 : service.missing,
260
+ result: latest[service.key],
261
+ stats: configured ? stats[service.key] : void 0
262
+ };
263
+ };
264
+ const checkedAts = Object.values(latest).map((result) => result.checkedAt);
265
+ return {
266
+ schemaVersion: 1,
267
+ generatedAt: at,
268
+ lastCheckedAt: checkedAts.length ? Math.max(...checkedAts) : null,
269
+ lastRunAt: lastRun?.at ?? null,
270
+ lastRunTrigger: lastRun?.trigger ?? null,
271
+ nextDueAt: lastRun ? lastRun.at + intervalMs : null,
272
+ intervalMs,
273
+ retentionMs,
274
+ ...scoreOf(latest),
275
+ tree: tree.map(toSnapshot)
276
+ };
277
+ },
278
+ async history({ range = "24h" } = {}) {
279
+ const at = now();
280
+ const runs = await store.runs({ since: at - chunkNP3YYVRZ_cjs.RANGES[range].durationMs, until: at });
281
+ return {
282
+ schemaVersion: 1,
283
+ range,
284
+ bucketMs: chunkNP3YYVRZ_cjs.RANGES[range].bucketMs,
285
+ bars: chunkNP3YYVRZ_cjs.bucketRuns({ runs, range, now: at, scoring })
286
+ };
287
+ },
288
+ async serviceHistory(key, { range = "24h" } = {}) {
289
+ if (!services.has(key)) throw new Error(`sermon-js: unknown service key: ${key}`);
290
+ const at = now();
291
+ const samples = await store.samples(key, { since: at - chunkNP3YYVRZ_cjs.RANGES[range].durationMs, until: at });
292
+ return { schemaVersion: 1, key, range, samples };
293
+ }
294
+ };
295
+ return status;
296
+ }
297
+
298
+ // src/scheduler.ts
299
+ function startScheduler(status, options = {}) {
300
+ const periodMs = chunkNP3YYVRZ_cjs.toMs(options.every ?? status.intervalMs);
301
+ const jitter = Math.max(0, options.jitter ?? 0.1);
302
+ let timer;
303
+ let stopped = false;
304
+ const schedule = (delayMs) => {
305
+ if (stopped) return;
306
+ timer = setTimeout(run, delayMs);
307
+ timer.unref?.();
308
+ };
309
+ const run = async () => {
310
+ try {
311
+ const result = await status.tick();
312
+ options.onTick?.(result);
313
+ } catch (error) {
314
+ options.onError?.(error);
315
+ }
316
+ schedule(periodMs + Math.random() * jitter * periodMs);
317
+ };
318
+ schedule(options.immediate === false ? periodMs : 0);
319
+ return {
320
+ stop() {
321
+ stopped = true;
322
+ clearTimeout(timer);
323
+ }
324
+ };
325
+ }
326
+
327
+ Object.defineProperty(exports, "DEFAULT_SCORING", {
328
+ enumerable: true,
329
+ get: function () { return chunkNP3YYVRZ_cjs.DEFAULT_SCORING; }
330
+ });
331
+ Object.defineProperty(exports, "RANGES", {
332
+ enumerable: true,
333
+ get: function () { return chunkNP3YYVRZ_cjs.RANGES; }
334
+ });
335
+ Object.defineProperty(exports, "bucketRuns", {
336
+ enumerable: true,
337
+ get: function () { return chunkNP3YYVRZ_cjs.bucketRuns; }
338
+ });
339
+ Object.defineProperty(exports, "computeScore", {
340
+ enumerable: true,
341
+ get: function () { return chunkNP3YYVRZ_cjs.computeScore; }
342
+ });
343
+ Object.defineProperty(exports, "computeServiceStats", {
344
+ enumerable: true,
345
+ get: function () { return chunkNP3YYVRZ_cjs.computeServiceStats; }
346
+ });
347
+ Object.defineProperty(exports, "labelForScore", {
348
+ enumerable: true,
349
+ get: function () { return chunkNP3YYVRZ_cjs.labelForScore; }
350
+ });
351
+ Object.defineProperty(exports, "toMs", {
352
+ enumerable: true,
353
+ get: function () { return chunkNP3YYVRZ_cjs.toMs; }
354
+ });
355
+ Object.defineProperty(exports, "generic", {
356
+ enumerable: true,
357
+ get: function () { return chunkELV4ZU6G_cjs.generic; }
358
+ });
359
+ Object.defineProperty(exports, "collectSecretValues", {
360
+ enumerable: true,
361
+ get: function () { return chunkPARE6EUZ_cjs.collectSecretValues; }
362
+ });
363
+ Object.defineProperty(exports, "sanitizeError", {
364
+ enumerable: true,
365
+ get: function () { return chunkPARE6EUZ_cjs.sanitizeError; }
366
+ });
367
+ Object.defineProperty(exports, "DEFAULT_SLOW_MS", {
368
+ enumerable: true,
369
+ get: function () { return chunkXHXEKB3Q_cjs.DEFAULT_SLOW_MS; }
370
+ });
371
+ Object.defineProperty(exports, "DEFAULT_TIMEOUT_MS", {
372
+ enumerable: true,
373
+ get: function () { return chunkXHXEKB3Q_cjs.DEFAULT_TIMEOUT_MS; }
374
+ });
375
+ Object.defineProperty(exports, "combine", {
376
+ enumerable: true,
377
+ get: function () { return chunkXHXEKB3Q_cjs.combine; }
378
+ });
379
+ Object.defineProperty(exports, "createHttp", {
380
+ enumerable: true,
381
+ get: function () { return chunkXHXEKB3Q_cjs.createHttp; }
382
+ });
383
+ Object.defineProperty(exports, "custom", {
384
+ enumerable: true,
385
+ get: function () { return chunkXHXEKB3Q_cjs.custom; }
386
+ });
387
+ Object.defineProperty(exports, "definePreset", {
388
+ enumerable: true,
389
+ get: function () { return chunkXHXEKB3Q_cjs.definePreset; }
390
+ });
391
+ Object.defineProperty(exports, "errorMessage", {
392
+ enumerable: true,
393
+ get: function () { return chunkXHXEKB3Q_cjs.errorMessage; }
394
+ });
395
+ Object.defineProperty(exports, "group", {
396
+ enumerable: true,
397
+ get: function () { return chunkXHXEKB3Q_cjs.group; }
398
+ });
399
+ Object.defineProperty(exports, "measure", {
400
+ enumerable: true,
401
+ get: function () { return chunkXHXEKB3Q_cjs.measure; }
402
+ });
403
+ exports.defineStatus = defineStatus;
404
+ exports.memoryStore = memoryStore;
405
+ exports.startScheduler = startScheduler;
406
+ //# sourceMappingURL=index.cjs.map
407
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stores/memory.ts","../src/status.ts","../src/scheduler.ts"],"names":["resolveTree","toMs","resolveScoring","collectSecretValues","computeScore","computeServiceStats","redactText","runServices","RANGES","bucketRuns"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { CheckSample, LockResult, RunSummary, StatusStore } from \"../store\";\nimport type { CheckResult } from \"../types\";\n\n/**\n * In-memory store: the default when no store is given. History lives only as long as the\n * process, so use a persistent store on serverless platforms.\n */\nexport function memoryStore({ now = () => Date.now() }: { now?: () => number } = {}): StatusStore {\n let latest: Record<string, CheckResult> = {};\n let runs: RunSummary[] = [];\n const samples = new Map<string, CheckSample[]>();\n const locks = new Map<string, number>();\n const cache = new Map<string, { value: string; expiresAt: number }>();\n\n return {\n async saveRun(run) {\n for (const result of run.results) latest[result.key] = result;\n for (const sample of run.samples) {\n const list = samples.get(sample.key) ?? [];\n list.push(sample);\n samples.set(sample.key, list);\n }\n runs.push(run.summary);\n },\n\n async latest() {\n return { ...latest };\n },\n\n async runs({ since, until }) {\n return runs.filter((run) => run.at >= since && run.at <= until).sort((a, b) => a.at - b.at);\n },\n\n async samples(key, { since, until }) {\n return (samples.get(key) ?? [])\n .filter((sample) => sample.at >= since && sample.at <= until)\n .sort((a, b) => a.at - b.at);\n },\n\n async prune({ before, keepKeys }) {\n runs = runs.filter((run) => run.at >= before);\n for (const [key, list] of samples) {\n const kept = list.filter((sample) => sample.at >= before);\n if (kept.length) samples.set(key, kept);\n else samples.delete(key);\n }\n if (keepKeys) {\n const keep = new Set(keepKeys);\n latest = Object.fromEntries(Object.entries(latest).filter(([key]) => keep.has(key)));\n for (const key of [...samples.keys()]) if (!keep.has(key)) samples.delete(key);\n }\n },\n\n async lock(name, ttlMs): Promise<LockResult> {\n const current = now();\n const expiresAt = locks.get(name);\n if (expiresAt !== undefined && expiresAt > current) {\n return { ok: false, retryAfterMs: expiresAt - current };\n }\n locks.set(name, current + ttlMs);\n return { ok: true };\n },\n\n async unlock(name) {\n locks.delete(name);\n },\n\n cache: {\n async get(key) {\n const entry = cache.get(key);\n if (!entry) return null;\n if (entry.expiresAt <= now()) {\n cache.delete(key);\n return null;\n }\n return entry.value;\n },\n async set(key, value, ttlMs) {\n cache.set(key, { value, expiresAt: now() + ttlMs });\n },\n },\n };\n}\n","import { resolveTree, type ResolvedNode, type ResolvedService, type ServiceNode } from \"./config\";\nimport { toMs, type Duration } from \"./duration\";\nimport { bucketRuns, computeServiceStats, RANGES } from \"./history\";\nimport { runServices } from \"./runner\";\nimport { collectSecretValues, redactText } from \"./sanitize\";\nimport { computeScore, resolveScoring, type Scoring, type ScoringInput } from \"./score\";\nimport type { CheckSample, LockResult, RunSummary, StatusStore } from \"./store\";\nimport { memoryStore } from \"./stores/memory\";\nimport type {\n CheckCache,\n CheckResult,\n HistoryPayload,\n HistoryRange,\n ScoreLabel,\n ServiceStats,\n SnapshotNode,\n StatusSnapshot,\n Trigger,\n} from \"./types\";\n\nexport type StatusConfig = {\n /** What to supervise: services built with presets or `custom()`, optionally nested in `group()`s */\n services: ServiceNode[];\n /** Where history is kept. Defaults to memory, which lasts as long as the process. */\n store?: StatusStore;\n /** How often a full run is due (default \"5m\"). Used by `tick()`, the scheduler and staleness. */\n interval?: Duration;\n /** How long history is kept (default \"7d\") */\n retention?: Duration;\n scoring?: ScoringInput;\n /**\n * Never quote what a service replied in public errors: a failed HTTP answer is reported by\n * its status code only (\"HTTP 500\"). Turn it on when a checked service could answer with\n * something sensitive, such as a debug page. Default false. Errors returned by `custom()`\n * checks are yours and are kept (redacted) either way.\n */\n hideResponseBodies?: boolean;\n /** Receives configuration warnings (default: `console.warn`) */\n onWarning?: (message: string) => void;\n /** Override `fetch` (tests, custom agents) */\n fetch?: typeof fetch;\n /** Override the clock (tests) */\n now?: () => number;\n};\n\nexport type RunReport = {\n at: number;\n trigger: Trigger;\n /** True when every service was checked */\n full: boolean;\n score: number | null;\n label: ScoreLabel;\n results: CheckResult[];\n};\n\nexport type ServiceHistory = { schemaVersion: 1; key: string; range: HistoryRange; samples: CheckSample[] };\n\nexport type TickResult =\n | { ran: true; report: RunReport }\n | {\n ran: false;\n /** \"not-due\": the last full run is recent enough. \"locked\": another tick is running. */\n reason: \"not-due\" | \"locked\";\n /** When the next full run is due (ms epoch) */\n nextDueAt: number;\n };\n\nexport type Status = {\n /** Check every service (or only `keys`) now, and record the results */\n run(options?: { keys?: string[]; trigger?: Trigger }): Promise<RunReport>;\n /**\n * Run every service only if a full run is due (the last one is older than `interval`), and\n * never twice at once. Safe to call as often as you like, from any number of triggers:\n * a cron, a platform scheduler, a timer, a page view.\n */\n tick(options?: { force?: boolean }): Promise<TickResult>;\n /** Everything a status page needs: the tree with latest results, statistics and the score */\n snapshot(): Promise<StatusSnapshot>;\n /** Score history in fixed buckets */\n history(options?: { range?: HistoryRange }): Promise<HistoryPayload>;\n /** Raw samples of one service */\n serviceHistory(key: string, options?: { range?: HistoryRange }): Promise<ServiceHistory>;\n /**\n * Remove every configured secret (and anything shaped like a credential) from a text.\n * Use it before logging errors that come from checks, stores or drivers.\n */\n redact(text: string): string;\n /** Whether a service with this key exists */\n has(key: string): boolean;\n /** Take a named lock for `ttlMs` (the store's when it has one, otherwise in-process) */\n lock(name: string, ttlMs: number): Promise<LockResult>;\n /** When the last full run happened, if any within the retention window */\n lastFullRun(): Promise<{ at: number; trigger: Trigger } | null>;\n readonly services: ReadonlyMap<string, ResolvedService>;\n readonly tree: readonly ResolvedNode[];\n readonly store: StatusStore;\n readonly intervalMs: number;\n readonly retentionMs: number;\n readonly scoring: Scoring;\n readonly warnings: readonly string[];\n};\n\nconst TICK_LOCK = \"sermon:tick\";\nconst STATS_KEY = \"sermon:stats\";\nconst LAST_RUN_KEY = \"sermon:last-full-run\";\n\nfunction sampleOf(result: CheckResult): CheckSample {\n const healthy = result.status === \"up\" || result.status === \"sleeping\";\n return {\n key: result.key,\n at: result.checkedAt,\n status: result.status,\n latencyMs: result.latencyMs,\n httpStatus: result.httpStatus,\n error: healthy ? undefined : result.error,\n instances: result.instances?.length\n ? Object.fromEntries(\n result.instances.map((instance) => [instance.key, [instance.status, instance.latencyMs]])\n )\n : undefined,\n };\n}\n\nfunction parseJson<T>(value: string | null): T | undefined {\n if (!value) return undefined;\n try {\n return JSON.parse(value) as T;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Describe what to supervise and get a status engine: run checks, read snapshots and history.\n * Nothing here depends on a host or a framework; see \"sermon-js/api\" to expose it over HTTP.\n */\nexport function defineStatus(config: StatusConfig): Status {\n const now = config.now ?? (() => Date.now());\n const resolved = resolveTree(config.services ?? []);\n const { services, tree } = resolved;\n const store = config.store ?? memoryStore({ now });\n const intervalMs = toMs(config.interval ?? \"5m\");\n const retentionMs = toMs(config.retention ?? \"7d\");\n const scoring = resolveScoring(config.scoring);\n const warn = config.onWarning ?? ((message: string) => console.warn(`[sermon-js] ${message}`));\n for (const warning of resolved.warnings) warn(warning);\n\n const secretValues = collectSecretValues(\n [...services.values()].flatMap((service) => [...Object.values(service.secrets), ...(service.sensitive ?? [])])\n );\n\n // Fallbacks for stores without a cache or a lock: fine for a single process\n const fallback = memoryStore({ now });\n const cache = store.cache ?? fallback.cache!;\n const lock = (name: string, ttlMs: number) =>\n store.lock ? store.lock(name, ttlMs) : fallback.lock!(name, ttlMs);\n const unlock = async (name: string) => {\n if (store.lock) await store.unlock?.(name);\n else await fallback.unlock!(name);\n };\n\n // A full run can't outlast its slowest check by much: that bounds how long the tick lock is held\n const longestCheckMs = Math.max(0, ...[...services.values()].map((service) => service.timeoutMs));\n const tickLockMs = longestCheckMs + 6_000;\n\n const checkCache = (key: string): CheckCache => ({\n get: (name) => cache.get(`sermon:check:${key}:${name}`),\n set: (name, value, ttlMs) => cache.set(`sermon:check:${key}:${name}`, value, ttlMs),\n });\n\n /** Latest results of the services that still exist; a service that lost its configuration drops its old result */\n async function currentLatest() {\n const stored = await store.latest();\n const latest: Record<string, CheckResult> = {};\n for (const [key, service] of services) {\n const result = stored[key];\n if (!result) continue;\n if (service.missing.length > 0 && result.status !== \"unconfigured\") continue;\n latest[key] = result;\n }\n return latest;\n }\n\n function scoreOf(latest: Record<string, CheckResult>) {\n return computeScore({\n scoring,\n results: [...services.values()]\n .filter((service) => latest[service.key])\n .map((service) => ({ status: latest[service.key]!.status, critical: service.critical })),\n });\n }\n\n async function readStats() {\n return parseJson<Record<string, ServiceStats>>(await cache.get(STATS_KEY)) ?? {};\n }\n\n /** Recompute the statistics of some services from their samples and cache them all together */\n async function refreshStats(keys: string[], known?: Record<string, ServiceStats>) {\n const at = now();\n const stats = known ?? (await readStats());\n await Promise.all(\n keys.map(async (key) => {\n const samples = await store.samples(key, { since: at - retentionMs, until: at });\n stats[key] = computeServiceStats({ samples, now: at });\n })\n );\n // Forget services that no longer exist\n for (const key of Object.keys(stats)) if (!services.has(key)) delete stats[key];\n await cache.set(STATS_KEY, JSON.stringify(stats), retentionMs);\n return stats;\n }\n\n async function lastFullRun() {\n const cached = parseJson<{ at: number; trigger: Trigger }>(await cache.get(LAST_RUN_KEY));\n if (cached) return cached;\n const at = now();\n const runs = await store.runs({ since: at - retentionMs, until: at });\n const last = [...runs].reverse().find((run) => run.full);\n return last ? { at: last.at, trigger: last.trigger } : null;\n }\n\n const status: Status = {\n services,\n tree,\n store,\n intervalMs,\n retentionMs,\n scoring,\n warnings: resolved.warnings,\n redact: (text) => redactText({ text, secretValues }),\n has: (key) => services.has(key),\n lock,\n lastFullRun,\n\n async run({ keys, trigger = \"manual\" } = {}) {\n const unknown = (keys ?? []).filter((key) => !services.has(key));\n if (unknown.length) throw new Error(`sermon-js: unknown service key(s): ${unknown.join(\", \")}`);\n const selected = keys ? keys.map((key) => services.get(key)!) : [...services.values()];\n const full = !keys;\n\n const results = await runServices({\n services: selected,\n secretValues,\n cacheFor: checkCache,\n now,\n fetch: config.fetch,\n hideResponseBodies: config.hideResponseBodies,\n });\n\n // The overall score always reflects the whole system: merge with what we already know\n const latest = full ? {} : await currentLatest();\n for (const result of results) latest[result.key] = result;\n const { score, label } = scoreOf(latest);\n\n const at = now();\n const failing: RunSummary[\"failing\"] = {};\n for (const result of results) {\n if (result.status === \"down\" || result.status === \"degraded\") failing[result.key] = result.status;\n }\n const summary: RunSummary = { at, trigger, full, score, label, failing };\n const samples = results.filter((result) => result.status !== \"unconfigured\").map(sampleOf);\n\n await store.saveRun({ summary, results, samples });\n if (full) {\n await store.prune({ before: at - retentionMs, keepKeys: [...services.keys()] });\n await cache.set(LAST_RUN_KEY, JSON.stringify({ at, trigger }), retentionMs);\n }\n await refreshStats(samples.map((sample) => sample.key));\n\n return { at, trigger, full, score, label, results };\n },\n\n async tick({ force = false } = {}) {\n const at = now();\n // Schedulers fire a little early or late: a run counts as due slightly before the interval ends\n const tolerance = Math.min(intervalMs * 0.1, 30_000);\n const last = await lastFullRun();\n if (!force && last && at - last.at < intervalMs - tolerance) {\n return { ran: false, reason: \"not-due\", nextDueAt: last.at + intervalMs };\n }\n // Only one run at a time. The lock expires by itself if a run dies, and is released as soon\n // as a run ends, so the \"due\" check above is what paces the runs.\n const held = await lock(TICK_LOCK, tickLockMs);\n if (!held.ok) {\n return { ran: false, reason: \"locked\", nextDueAt: at + held.retryAfterMs };\n }\n try {\n // Another instance may have finished a run between our check and taking the lock\n const latest = force ? null : await lastFullRun();\n if (latest && now() - latest.at < intervalMs - tolerance) {\n return { ran: false, reason: \"not-due\", nextDueAt: latest.at + intervalMs };\n }\n return { ran: true, report: await status.run({ trigger: \"scheduled\" }) };\n } finally {\n await unlock(TICK_LOCK).catch(() => undefined);\n }\n },\n\n async snapshot() {\n const at = now();\n const [latest, cachedStats, lastRun] = await Promise.all([currentLatest(), readStats(), lastFullRun()]);\n\n // Statistics are normally cached at run time; rebuild whatever is missing (cold cache)\n const missing = Object.values(latest)\n .filter((result) => result.status !== \"unconfigured\" && !cachedStats[result.key])\n .map((result) => result.key);\n const stats = missing.length ? await refreshStats(missing, cachedStats) : cachedStats;\n\n const toSnapshot = (node: ResolvedNode): SnapshotNode => {\n if (node.type === \"group\") {\n return {\n type: \"group\",\n key: node.key,\n title: node.title,\n description: node.description,\n children: node.children.map(toSnapshot),\n };\n }\n const service = services.get(node.key)!;\n const configured = service.missing.length === 0;\n return {\n type: \"service\",\n key: service.key,\n kind: service.kind,\n name: service.name,\n description: service.description,\n critical: service.critical,\n slowMs: service.slowMs,\n configured,\n missing: configured ? undefined : service.missing,\n result: latest[service.key],\n stats: configured ? stats[service.key] : undefined,\n };\n };\n\n const checkedAts = Object.values(latest).map((result) => result.checkedAt);\n return {\n schemaVersion: 1,\n generatedAt: at,\n lastCheckedAt: checkedAts.length ? Math.max(...checkedAts) : null,\n lastRunAt: lastRun?.at ?? null,\n lastRunTrigger: lastRun?.trigger ?? null,\n nextDueAt: lastRun ? lastRun.at + intervalMs : null,\n intervalMs,\n retentionMs,\n ...scoreOf(latest),\n tree: tree.map(toSnapshot),\n };\n },\n\n async history({ range = \"24h\" } = {}) {\n const at = now();\n const runs = await store.runs({ since: at - RANGES[range].durationMs, until: at });\n return {\n schemaVersion: 1,\n range,\n bucketMs: RANGES[range].bucketMs,\n bars: bucketRuns({ runs, range, now: at, scoring }),\n };\n },\n\n async serviceHistory(key, { range = \"24h\" } = {}) {\n if (!services.has(key)) throw new Error(`sermon-js: unknown service key: ${key}`);\n const at = now();\n const samples = await store.samples(key, { since: at - RANGES[range].durationMs, until: at });\n return { schemaVersion: 1, key, range, samples };\n },\n };\n\n return status;\n}\n","import { toMs, type Duration } from \"./duration\";\nimport type { Status, TickResult } from \"./status\";\n\nexport type SchedulerOptions = {\n /** How often to call `tick()` (default: the status `interval`) */\n every?: Duration;\n /** Run a first tick right away instead of waiting one period (default true) */\n immediate?: boolean;\n /** Random extra delay per period, as a share of it, so replicas don't all fire together (default 0.1) */\n jitter?: number;\n onTick?: (result: TickResult) => void;\n /** Called when a tick throws (e.g. the store is unreachable). The scheduler keeps going. */\n onError?: (error: unknown) => void;\n};\n\n/**\n * In-process scheduler for long-running servers (Node, Bun, Deno, containers).\n * It only calls `status.tick()`, which is idempotent, so several replicas sharing one store\n * still run the checks once per interval.\n *\n * On serverless platforms use the platform's scheduler, or the `/tick` endpoint, instead.\n */\nexport function startScheduler(status: Status, options: SchedulerOptions = {}): { stop: () => void } {\n const periodMs = toMs(options.every ?? status.intervalMs);\n const jitter = Math.max(0, options.jitter ?? 0.1);\n let timer: ReturnType<typeof setTimeout> | undefined;\n let stopped = false;\n\n const schedule = (delayMs: number) => {\n if (stopped) return;\n timer = setTimeout(run, delayMs);\n // Don't keep a Node process alive just for the scheduler\n (timer as { unref?: () => void }).unref?.();\n };\n\n const run = async () => {\n try {\n // Tick first: `onTick?.(await …)` would skip the call entirely when there is no listener\n const result = await status.tick();\n options.onTick?.(result);\n } catch (error) {\n options.onError?.(error);\n }\n schedule(periodMs + Math.random() * jitter * periodMs);\n };\n\n schedule(options.immediate === false ? periodMs : 0);\n\n return {\n stop() {\n stopped = true;\n clearTimeout(timer);\n },\n };\n}\n"]}