UNPKG

homebridge-nest-accfactory

Version:

Homebridge support for Nest/Google devices including HomeKit Secure Video (HKSV) support for doorbells and cameras

260 lines (210 loc) 7.32 kB
// RingBuffer // Part of homebridge-nest-accfactory // // Dynamically-sized circular buffer with logical indexing. // // Features: // - O(1) push and shift operations // - Dynamic capacity growth up to configured maximum // - Monotonically increasing logical indexing // - Zero-copy trimming using circular head movement // - Preallocated storage to reduce allocation churn // - Logical index and offset-based access helpers // - Iterable support for for...of, Array.from(), and spread syntax // // Terminology: // - head: physical index of the first logical item // - size: number of retained items // - startIndex: logical index of the first retained item // - tailIndex: logical index immediately after the retained window // // Access is performed using logical offsets (0..size-1), // which are translated to physical storage locations // using modulo arithmetic. // // Intended use: // - Timeline buffers // - RTP/media packet queues // - Jitter buffers // - Rolling history windows // // Code version 2026.05.17 // Mark Hulskamp 'use strict'; // Define constants const RINGBUFFER_DEFAULT_CAPACITY = 1024; const RINGBUFFER_MAX_CAPACITY = 65535; // Arbitrary upper limit to prevent unbounded growth. Adjust as needed. // RingBuffer object export default class RingBuffer { items = []; capacity = 0; maxCapacity = RINGBUFFER_MAX_CAPACITY; head = 0; size = 0; startIndex = 0; constructor(startIndex = 0, capacity = RINGBUFFER_DEFAULT_CAPACITY, maxCapacity = RINGBUFFER_MAX_CAPACITY) { // Clamp maximum capacity to a sane positive integer. // Allows generic callers to tune growth limits without changing module defaults. this.maxCapacity = Number.isInteger(maxCapacity) === true && maxCapacity > 0 ? maxCapacity : RINGBUFFER_MAX_CAPACITY; // Clamp caller supplied capacity to configured limits. // If capacity exceeds maxCapacity, maxCapacity wins because this buffer // must never grow beyond the configured ceiling. this.capacity = Number.isInteger(capacity) === true && capacity > 0 ? Math.min(capacity, this.maxCapacity) : Math.min(RINGBUFFER_DEFAULT_CAPACITY, this.maxCapacity); // Preallocate backing array so push operations avoid repeated array growth allocations. this.items = new Array(this.capacity); // Logical index of the first retained item. // Used externally to preserve cursor stability even when items are trimmed. this.startIndex = Number.isInteger(startIndex) === true && startIndex >= 0 ? startIndex : 0; } get empty() { // Convenience helper for callers. return this.size === 0; } get full() { // Convenience helper for callers. return this.size >= this.capacity; } get tailIndex() { // Logical index immediately after the retained window. return this.startIndex + this.size; } [Symbol.iterator]() { let index = 0; // Allow RingBuffer to be used with native iteration helpers: // for...of, Array.from(), spread syntax, etc. // This is intended as a convenience API, not as the hot-path access method // for streaming loops where allocation control matters. return { next: () => { if (index < this.size) { return { value: this.getByOffset(index++), done: false, }; } return { done: true }; }, }; } physicalOffset(offset) { // Convert a logical offset into a physical storage index. // // head = physical location of logical offset 0. // Wrap using modulo arithmetic so storage behaves as a circular array. if (Number.isInteger(offset) !== true || offset < 0) { return -1; } return (this.head + offset) % this.capacity; } getByOffset(offset) { let physicalOffset = 0; // Bounds check against retained logical size. if (Number.isInteger(offset) !== true || offset < 0 || offset >= this.size) { return undefined; } // Translate logical offset -> physical storage slot. physicalOffset = this.physicalOffset(offset); return physicalOffset >= 0 ? this.items[physicalOffset] : undefined; } at(logicalIndex) { // Resolve an item by logical index rather than offset. // Useful for callers working with timeline indexes. if (Number.isInteger(logicalIndex) !== true || logicalIndex < this.startIndex || logicalIndex >= this.startIndex + this.size) { return undefined; } return this.getByOffset(logicalIndex - this.startIndex); } grow() { let newCapacity = 0; let newItems = undefined; let index = 0; // Prevent unbounded growth. // Once the maximum configured capacity is reached, expansion is rejected. if (this.capacity >= this.maxCapacity) { return false; } // Double capacity to amortize resize cost. // Clamp to configured maximum. newCapacity = Math.min(this.capacity * 2, this.maxCapacity); newItems = new Array(newCapacity); // Re-pack retained items linearly starting at index 0. // Removes physical wrap-around and simplifies indexing. while (index < this.size) { newItems[index] = this.getByOffset(index); index++; } this.items = newItems; this.capacity = newCapacity; // Logical offset 0 now begins at physical slot 0. this.head = 0; return true; } push(item) { let tailOffset = 0; // Expand if the retained window has filled capacity. if (this.size >= this.capacity) { if (this.grow() !== true) { return false; } } // Tail = logical offset "size". // Convert into physical storage slot. tailOffset = (this.head + this.size) % this.capacity; // Insert at tail and expand retained size. this.items[tailOffset] = item; this.size++; return true; } shift(count, resetStartIndex = undefined) { let removeCount = 0; let index = 0; let physicalOffset = 0; // Remove N items from the logical front. if (Number.isInteger(count) !== true || count <= 0) { return false; } removeCount = Math.min(count, this.size); // Remove retained references so GC can reclaim memory. while (index < removeCount) { physicalOffset = this.physicalOffset(index); if (physicalOffset >= 0) { this.items[physicalOffset] = undefined; } index++; } // Advance retained head position. this.head = (this.head + removeCount) % this.capacity; this.size -= removeCount; this.startIndex += removeCount; // Reset internal state once fully emptied. if (this.size === 0) { this.head = 0; if (Number.isInteger(resetStartIndex) === true && resetStartIndex >= 0) { this.startIndex = resetStartIndex; } } return true; } clear(resetStartIndex = 0) { let index = 0; let physicalOffset = 0; // Clear retained references. while (index < this.size) { physicalOffset = this.physicalOffset(index); if (physicalOffset >= 0) { this.items[physicalOffset] = undefined; } index++; } // Reset retained state. this.head = 0; this.size = 0; if (Number.isInteger(resetStartIndex) === true && resetStartIndex >= 0) { this.startIndex = resetStartIndex; } return true; } }