Lumi/plugins/lumi_transcription/backend/sessions/bounded_queue.js
2026-07-22 11:01:49 +02:00

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 };