import type { SubscriptionRecord } from "@uptimizr/db"; import type { CollectorConfig } from "../config.js"; import type { LiveBus } from "../liveBus.js"; import type { CollectorStore } from "../store.js"; import { type EvaluationResult } from "./evaluate.js"; import type { SubscriptionStream } from "./stream.js"; /** * **The subscription scheduler** (#311, ADR 0051 §6 / sketch §F.2). * * One in-process scheduler for the whole collector. Its job is small and its * constraints are the interesting part: * * - **Every subscription owns one timer**, firing on its own `evaluate.every`. * Even the bus-backed predicates have one: `presence <= 0` ("nobody is in the * scene") can only become true when events *stop* arriving, so a scheduler * that only woke on bus traffic could never report it. * - **The bus is an accelerator, not the clock.** `presence` and `new_value` * additionally watch `LiveBus.subscribe` so they react in seconds rather than * at the next tick. The state each one keeps is bounded * ({@link NEW_VALUE_MAX_TRIGGERS}) and the trigger is rate-limited * ({@link BUS_MIN_INTERVAL_MS}), so a firehose cannot turn into a query storm. * The *answer* still comes from the store evaluation, so the bus can only * change when a subscription is checked, never what it concludes. * - **At most {@link CollectorConfig.subscriptionsMaxConcurrent} evaluations run * at once**, and at most one per subscription. A tick that arrives while the * previous one is still in flight is dropped, not queued — a subscription that * cannot keep up with its own interval must not grow a backlog. * - **Cooldown is enforced before delivery, not before evaluation.** The * evaluation is cheap and its result is what `POST …/test` and the dashboard * read; the *firing* is what costs a webhook. * - **Every timer is `unref`'d and cleared by {@link SubscriptionScheduler.stop}.** * A test that starts the scheduler and closes the app leaves nothing behind. */ /** Smallest gap between two bus-triggered evaluations of one subscription. */ export declare const BUS_MIN_INTERVAL_MS = 5000; /** * Distinct dimension values one `new_value` watcher remembers having triggered * on. Past this the set is cleared and the watcher falls back to its timer — the * predicate is for low-cardinality dimensions ("a new scene", "a new custom * event"), and an unbounded set on a hot bus is exactly the kind of state * ADR 0032 §6 exists to prevent. */ export declare const NEW_VALUE_MAX_TRIGGERS = 256; /** How often the scheduler re-reads the store to pick up external changes. */ export declare const RELOAD_INTERVAL_MS = 60000; export interface SchedulerDeps { store: CollectorStore; config: CollectorConfig; liveBus: LiveBus; stream: SubscriptionStream; /** Structured logger; a Fastify instance's `log` in production. */ log: { warn: (obj: unknown, msg?: string) => void; info: (obj: unknown, msg?: string) => void; }; /** Injected for tests. Defaults to the global `fetch`. */ fetchImpl?: typeof fetch; /** Injected for tests. Defaults to `Date.now`. */ now?: () => number; /** Injected for tests, so webhook backoff does not sleep for real. */ sleep?: (ms: number) => Promise; /** * Fraction in `[0, 1)` used to jitter a subscription's first evaluation. * Defaults to `Math.random`; tests pass `() => 0` for a deterministic phase. */ jitter?: () => number; } export interface RunOptions { /** Deliver a firing (record it, fan it out, POST it). `false` for a dry run. */ deliver?: boolean; } export interface SubscriptionScheduler { /** Reconcile timers and bus watchers against the store. Idempotent. */ reload(): Promise; /** * Evaluate one subscription now, outside its schedule. Returns the evaluation * so `POST …/test` can answer with it. Honours cooldown when delivering. * * `null` means "not now": the subscription already had an evaluation in * flight, or the collector was at its concurrency cap. The route turns that * into a `409` rather than quietly running a second one in parallel. */ runOnce(sub: SubscriptionRecord, options?: RunOptions): Promise; /** Subscriptions currently scheduled. */ readonly scheduledCount: number; /** Evaluations in flight (for tests asserting the concurrency cap). */ readonly inFlight: number; /** Clear every timer and bus watcher. Idempotent. */ stop(): void; } /** * Create the scheduler. It does nothing until {@link SubscriptionScheduler.reload} * is called, which `app.ts` does once at startup and every CRUD write does after * it changes something. */ export declare function createSubscriptionScheduler(deps: SchedulerDeps): SubscriptionScheduler; //# sourceMappingURL=scheduler.d.ts.map