/** * In-memory recent-window buffer for the live telemetry stream: the records a client replays on * subscribe and the records the stats view is computed from. One buffer per process, partitioned by * strategy address — the same shape as the disk ring's process-global sink map, because the tap that * feeds it (`appendDomainEvent`) is process-global too. * * Three things make a client's state recoverable across a reconnect: * - `epoch`, a ULID minted when the buffer is created. A change means the plugin restarted, not that * frames were lost. * - `seq`, monotonic per epoch across every address, so `(epoch, seq)` is a dedupe and resume key and * a single `afterSeq` cursor is unambiguous. It never rewinds while the epoch holds — not on * teardown, not on eviction. * - {@link TimelineBuffer.oldestTs}, the stamp of the oldest record still held. A client whose cursor * predates it knows it has a gap that no replay can fill. * * The write path is on the serial trading path: it takes one bounded slot write and returns. It does * no I/O, awaits nothing, computes nothing, and never throws. */ import type { AttrValue } from "../utils/event-catalog.js"; import { type LogLevel } from "../utils/logger.js"; /** * Records held per address before the oldest is overwritten. The bound is a record COUNT, per * address: a fixed-size ring makes eviction a single slot write (no scan, no shift) and makes the * oldest stamp an O(1) read, both of which a byte or age bound would cost the write path. Per address * rather than process-wide so one chatty strategy cannot evict a quiet one's window. * * This is the number to change when the retention window is measured — see also the * `maxRecordsPerAddress` option, which overrides it per buffer. */ export declare const DEFAULT_MAX_RECORDS_PER_ADDRESS = 2000; /** The seq of the first record of an epoch. Records start at 1, so `afterSeq: 0` means "everything". */ export declare const FIRST_SEQ = 1; /** One record as the tap hands it over, before the buffer stamps a seq. */ export interface TimelineRecordInput { /** Call-time epoch ms, stamped by `logger.event`. */ ts: number; name: string; level: LogLevel; address: string; /** The engine scanner id. Absent — never empty — on a record that has no scanner. */ scannerId?: string; body: string; attrs: Record; } /** A buffered record: the tap's input plus its position in the epoch. */ export interface TimelineRecord extends TimelineRecordInput { seq: number; } /** Filters for {@link TimelineBuffer.read}; all optional, all combinable. Time bounds are inclusive. */ export interface TimelineQuery { /** Exclusive lower bound on seq — the client's resume cursor. */ afterSeq?: number; address?: string; scannerId?: string; sinceMs?: number; untilMs?: number; /** Keep the last N after filtering (newest tail). */ limit?: number; } export interface TimelineBufferOptions { /** Overrides {@link DEFAULT_MAX_RECORDS_PER_ADDRESS}. Clamped to at least one record. */ maxRecordsPerAddress?: number; } export interface TimelineBuffer { /** ULID minted with this buffer. Rides every published payload. */ readonly epoch: string; /** Copy one record in and return. Never blocks, never throws. */ append(input: TimelineRecordInput): void; /** Records matching `query`, oldest first. */ read(query?: TimelineQuery): TimelineRecord[]; /** Stamp of the oldest record still held across every address; undefined when nothing is held. */ oldestTs(): number | undefined; /** * The largest seq this buffer has thrown away, or zero when it has thrown away nothing. A client * resuming from a cursor BELOW this lost at least one record; a cursor at or above it lost none. * * It is not the oldest seq still held. The rings are per address, so a quiet strategy keeps a low * seq alive long after a chatty one has evicted everything above it, and the oldest record held * then says nothing about what a resuming client is missing. */ highestDroppedSeq(): number; /** Records appended since the last drain, oldest first. Reads the publish frontier — does not evict. */ drain(): TimelineRecord[]; /** Drop every record. The seq counter is deliberately not rewound. */ teardown(): void; } export declare function createTimelineBuffer(options?: TimelineBufferOptions): TimelineBuffer; /** The process's telemetry buffer, minted (with its epoch) on first use. */ export declare function getTimelineBuffer(): TimelineBuffer; /** * Drop the process buffer on plugin reload or shutdown. The next {@link getTimelineBuffer} mints a * fresh epoch, so the restarted seq run cannot collide with the one a client already holds. */ export declare function teardownTimelineBuffer(): void; //# sourceMappingURL=timeline-buffer.d.ts.map