78 lines
2.8 KiB
JavaScript
78 lines
2.8 KiB
JavaScript
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 };
|