All files / core/semantic-log/src buffer.ts

100% Statements 63/63
100% Branches 10/10
100% Functions 7/7
100% Lines 63/63

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 641x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2459x 1x 1x 2458x 2458x 2458x 2458x 177x 177x 3x 3x 3x 177x 2458x 2458x 2458x 2458x 2458x 2458x 2458x  
/**
 * Bounded ring buffer of withheld detail (PRD R10; §5.1 rows "Backpressure and
 * drop policy" and "Sampling and rate limiting").
 *
 * R10 decouples verbosity from static level configuration: high-detail fields
 * are retained locally but *not transmitted*, and are flushed retroactively
 * when an escalation condition occurs (a local error, a failed assertion, an
 * anomaly). Nothing here leaves the process on its own — `take` is the only
 * drain — so the cost of withheld detail is bounded memory while the system is
 * healthy, and the detail is available the moment it is not. That is why
 * sampling and rate limiting are "Replaced": an anomaly is never sampled away,
 * it is escalated.
 *
 * The spec's drop policy is *explicit, observable accounting instead of silent
 * loss* (§5.1 "Backpressure and drop policy"). Overflow discards the oldest
 * entry and increments `dropped`; `dropped` is cumulative and never resets, so
 * `logger.ts` can report the loss on every record for as long as the buffer is
 * in use and no overflow can go unmentioned.
 *
 * Withheld detail is redacted *before* it is pushed (see `logger.ts`): Task 8
 * redacts at record-construction time so that a withheld value is absent from
 * everything retained, the ring buffer included. Retaining raw fields here
 * would reopen that hole, so the buffer only ever holds what the record would
 * have held.
 */
 
export interface RingBuffer<T> {
    /** Append `entry`, discarding the oldest entry if the bound is exceeded. */
    push(entry: T): void;
    /** A copy of the retained entries, oldest first; the buffer is unchanged. */
    peek(): T[];
    /** Remove and return every retained entry, oldest first. */
    take(): T[];
    /** How many entries are currently retained. */
    size(): number;
    /** How many entries have been discarded by the bound, cumulatively. */
    dropped(): number;
    /** Discard every retained entry; not an overflow, so `dropped` is unchanged. */
    clear(): void;
}
 
/** Create a ring buffer retaining at most `maxEntries` entries. */
export function createRingBuffer<T>(maxEntries: number): RingBuffer<T> {
    if (!Number.isInteger(maxEntries) || maxEntries < 1) {
        throw new Error('createRingBuffer: maxEntries must be a positive integer');
    }
    const entries: T[] = [];
    let dropped = 0;
    return {
        push(entry: T): void {
            entries.push(entry);
            while (entries.length > maxEntries) {
                entries.shift();
                dropped++;
            }
        },
        peek: () => [...entries],
        take: (): T[] => entries.splice(0, entries.length),
        size: () => entries.length,
        dropped: () => dropped,
        clear: (): void => void entries.splice(0, entries.length),
    };
}