class BoundedQueue { constructor(options = {}) { this.maxItems = positive(options.maxItems, 250); this.maxBytes = positive(options.maxBytes, 5 * 32000); this.maxAgeMs = positive(options.maxAgeMs, 5000); this.now = options.now || Date.now; this.items = []; this.bytes = 0; this.dropped = { capacity: 0, stale: 0 }; } push(value, options = {}) { const bytes = Math.max(0, Number(options.bytes ?? value?.length ?? value?.pcm?.length ?? 0)); const capturedAt = Number(options.capturedAt ?? this.now()); if (bytes > this.maxBytes) { this.dropped.capacity += 1; return false; } this.prune(); while (this.items.length >= this.maxItems || this.bytes + bytes > this.maxBytes) this.dropOldest("capacity"); this.items.push({ value, bytes, capturedAt }); this.bytes += bytes; return true; } shift() { this.prune(); const entry = this.items.shift(); if (!entry) return null; this.bytes -= entry.bytes; return entry.value; } prune() { const cutoff = this.now() - this.maxAgeMs; while (this.items[0] && this.items[0].capturedAt < cutoff) this.dropOldest("stale"); } clear() { this.items.length = 0; this.bytes = 0; } size() { this.prune(); return this.items.length; } metrics() { this.prune(); return { items: this.items.length, bytes: this.bytes, dropped: { ...this.dropped } }; } dropOldest(reason) { const entry = this.items.shift(); if (!entry) return; this.bytes -= entry.bytes; this.dropped[reason] += 1; } } class SequenceTracker { constructor() { this.last = null; this.gaps = 0; this.outOfOrder = 0; } accept(sequence) { const current = Number(sequence) >>> 0; if (this.last == null) { this.last = current; return { accepted: true, gap: 0 }; } if (current <= this.last) { this.outOfOrder += 1; return { accepted: false, gap: 0 }; } const gap = current - this.last - 1; if (gap) this.gaps += gap; this.last = current; return { accepted: true, gap }; } metrics() { return { last_sequence: this.last, sequence_gaps: this.gaps, out_of_order: this.outOfOrder }; } } class RollingPcmBuffer { constructor(options = {}) { this.queue = new BoundedQueue({ maxItems: options.maxItems || 300, maxBytes: (options.seconds || 5) * 32000, maxAgeMs: (options.seconds || 5) * 1000, now: options.now }); } push(frame, capturedAt) { return this.queue.push(Buffer.from(frame), { bytes: frame.length, capturedAt }); } snapshot() { this.queue.prune(); return Buffer.concat(this.queue.items.map((entry) => entry.value), this.queue.bytes); } metrics() { return this.queue.metrics(); } clear() { this.queue.clear(); } } function positive(value, fallback) { const number = Number(value); return Number.isFinite(number) && number > 0 ? number : fallback; } module.exports = { BoundedQueue, SequenceTracker, RollingPcmBuffer };