/** * Business volumes on a clock (#1750): calls per `(entityType, operation)` per time bucket, * counted over a scope's outbox. Which pairs are worth counting is the caller's, derived * from the model by the dashboard's `lifecycleMovesOf` (see `@substrat-run/contracts`'s * `operation-series.ts` for why those counts are exact). * * ## Bounded, and index-backed with no new index * * The window is capped at seven days and the answer at `OPERATION_SERIES_MAX_BUCKETS` per * pair, and it is ONE aggregate statement: no row leaves SQLite, so there is no per-row * work on this side of it. The rows it scans are a CLOSED range of the primary key, so the * work is the window's rows whatever lies after it — a window years ago visits only its own. * An event id is minted from the instant the operation ran (#956) by a mint whose floor only * rises, so an id never carries a time BEFORE its own `occurred_at`: every event at or after * `since` has an id at or above `ulidFloor(since)`. The key range ends at * `ulidFloor(until + OPERATION_SERIES_ID_SLACK_MS)`, a fixed sliver past the window. * `occurred_at` stays the exact filter on both ends; the key range only bounds the scan. * * The slack is for ids that run AHEAD of their instants. `createUlid` (ulid.ts) holds a * clock that is at or behind its floor at `lastTime` — and bumps a millisecond on the * (astronomically rare) random overflow — and `seedFrom` raises that floor to the outbox's * `MAX(id)` when a writer wakes. So an event in the window's last moments can carry an id * past `until`, by as much as the clock stepped back. The mint itself sets no bound on * that; the wall clock does: the Durable Object host reads the platform's NTP-disciplined * clock, whose corrections are sub-second. `OPERATION_SERIES_ID_SLACK_MS` covers that with * two orders of margin, at the cost of scanning one more minute of rows. `occurred_at` * stays the exact filter, so the slack admits rows to the scan, never to the count. * * Past the slack, stated: a step back larger than a minute (a pure host's injected clock * rewound by hand, a floor seeded from an id planted in the future) leaves an id beyond * the range, and that event is not counted by a window ending before its id. * * The bucket arithmetic casts its parameters: a driver may bind a JS number as REAL, and * then the division would place an event at its own instant instead of its bucket's start. * * The pairs go in as ONE bound JSON array, matched with `json_each`, rather than a * placeholder each: 64 pairs as two `IN` lists is 128 parameters, past the Durable * Object's 100. * * Same permission posture as every read in `timeline.ts`: the caller checks, this does not. */ import type { OperationSeriesInput, OperationSeriesResult } from '@substrat-run/contracts'; import type { TimelineReader } from './timeline.js'; /** * How far past `until` the key range reaches, for an id held at the mint's floor while its * instant was inside the window (see the header for where the size comes from). */ export declare const OPERATION_SERIES_ID_SLACK_MS = 60000; /** * The read's one statement and its parameters — exported so each adapter's suite can ask * its own SQLite for the plan of exactly what runs, not a copy of it. */ export declare function operationSeriesQuery(input: OperationSeriesInput): { sql: string; params: Array; }; export declare function readOperationSeries(ctx: TimelineReader, input: OperationSeriesInput): OperationSeriesResult; //# sourceMappingURL=operation-series.d.ts.map