homebridge-plugin-utils
Version:
Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.
137 lines • 5.78 kB
JavaScript
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
*
* backpressure.ts: Backpressure-aware write queue for Node.js writable streams.
*/
/**
* A backpressure-aware write queue that serializes writes to a writable stream, pausing when the stream signals backpressure and resuming on drain.
*
* The stream is resolved lazily via a getter function on each write, allowing the writer to be created before the stream exists and to handle stream replacement across
* process restarts.
*
* @example
*
* ```ts
* // Create a writer that feeds segments to an FFmpeg process stdin.
* const writer = new BackpressureWriter(() => ffmpegProcess.stdin ?? null);
*
* // Enqueue segments as they arrive from a livestream source.
* livestream.on("segment", (segment) => writer.write(segment));
*
* // When the session ends, close the writer to release pending data.
* writer.close();
* ```
*
* @category Utilities
*/
export class BackpressureWriter {
drainListener;
drainStream;
getStream;
isClosed;
isWriting;
onWrite;
queue;
/**
* Creates a new backpressure-aware write queue.
*
* @param getStream - A function that returns the current writable stream, or `null` if the stream is unavailable. Evaluated on each write attempt, allowing the
* writer to be created before the stream exists or to track a stream that changes across process restarts. For a static stream, wrap it in an
* arrow function: `() => stream`.
* @param onWrite - Optional. A callback invoked after each segment is successfully written to the underlying stream. Useful for tracking write statistics.
*
* @example
*
* ```ts
* // Lazy resolution...the stream is resolved on each write.
* const writer = new BackpressureWriter(() => this.ffmpegProcess?.stdin ?? null, () => segmentCount++);
*
* // Static stream...wrap in an arrow function.
* const writer = new BackpressureWriter(() => stream);
* ```
*/
constructor(getStream, onWrite) {
this.drainListener = null;
this.drainStream = null;
this.getStream = getStream;
this.isClosed = false;
this.isWriting = false;
this.onWrite = onWrite ?? null;
this.queue = [];
}
/**
* Enqueues data to be written to the stream. If the stream is available and not under backpressure, the data is written immediately. Otherwise, it is queued and
* written when the stream signals it is ready via the drain event.
*
* @param data - The buffer to write to the stream.
*
* @returns Returns `true` if the data was accepted (stream is available), `false` if the stream is unavailable or the writer has been closed.
*/
write(data) {
// If the writer has been closed or the stream is unavailable or not writable, reject the write.
const stream = this.isClosed ? null : this.getStream();
if (!stream?.writable) {
return false;
}
// Add the data to the queue and process it.
this.queue.push(data);
this.processQueue();
return true;
}
/**
* Closes the writer, clearing any pending data and removing drain listeners. After closing, all subsequent writes are rejected. This should be called when the
* underlying stream is being shut down or the session is ending.
*/
close() {
this.isClosed = true;
this.isWriting = false;
this.queue.length = 0;
// Remove any pending drain listener.
if (this.drainListener && this.drainStream) {
this.drainStream.off("drain", this.drainListener);
this.drainListener = null;
this.drainStream = null;
}
}
/**
* Returns the number of segments currently queued and waiting to be written.
*/
get pending() {
return this.queue.length;
}
// Process the write queue. Dequeues segments and writes them to the stream, respecting backpressure by waiting for drain events before continuing. The synchronous
// success path uses a loop rather than recursion to avoid growing the call stack when processing a burst of queued segments (e.g., initial timeshift buffer flush).
processQueue() {
// If we already have a write in progress, we're done...the drain listener will resume processing.
if (this.isWriting) {
return;
}
// Process as many queued segments as the stream can accept without backpressure.
while (this.queue.length) {
// Resolve the stream. If it's gone or no longer writable, we can't write.
const stream = this.isClosed ? null : this.getStream();
if (!stream?.writable) {
return;
}
// Dequeue and write.
this.isWriting = true;
const segment = this.queue.shift();
if (!stream.write(segment)) {
// The stream signaled backpressure. Wait for drain before processing the next segment.
this.drainStream = stream;
this.drainListener = () => {
this.drainStream = null;
this.drainListener = null;
this.isWriting = false;
this.onWrite?.();
this.processQueue();
};
stream.once("drain", this.drainListener);
return;
}
// Write succeeded immediately. Notify the caller and continue to the next segment.
this.isWriting = false;
this.onWrite?.();
}
}
}
//# sourceMappingURL=backpressure.js.map