rxprotoplex
Version:
A utility library for working with Plex-based connections and streams with RxJS operators.
137 lines (117 loc) • 4.45 kB
JavaScript
// asPlex.js
import {ReplaySubject} from 'rxjs';
import Protoplex from '@zacharygriffee/protoplex';
import {share, tap} from 'rxjs/operators';
/**
* Wraps a stream into a Protoplex instance and attaches a close$ observable.
*
* @param {Duplex} stream - The stream to be wrapped and managed.
* @param {Object} [config={}] - Optional configuration for Protoplex.
* @returns {Protoplex} - The wrapped Plex instance with a close$ observable.
*/
export const asPlex = (stream, config) => {
// Error handler to manage stream errors and avoid unhandled errors
const errorHandler = (err) => {
if (
stream &&
typeof stream.destroy === 'function' &&
!(stream.destroying || stream.destroyed)) {
stream.destroy(err); // Explicitly destroy the stream on error to prevent further events
}
};
const attachClose$ = (plex) => {
plex.close$ = createClose$(plex).pipe(
share(),
tap({
complete: () => {
if (plex.mux.stream &&
typeof plex.mux.stream.destroy === 'function' &&
!(stream.destroying || stream.destroyed)) {
plex.mux.stream.destroy();
}
},
error: (err) => {
if (plex.mux.stream &&
typeof plex.mux.stream.destroy === 'function' &&
!(stream.destroying || stream.destroyed)) {
plex.mux.stream.destroy(err);
}
}
})
);
};
// If the stream is already a Protoplex instance, return it as is
if (stream.isProtoplex) {
// Ensure close$ is attached even if asPlex is called multiple times
if (!stream.close$) {
attachClose$(stream);
}
return stream;
}
// Wrap the stream into a Protoplex instance
const plex = Protoplex.from(stream, config);
// Add the error handler to prevent unhandled error warnings
plex.mux.stream.on('error', errorHandler);
// Attach the close$ observable for the newly created Plex instance
attachClose$(plex);
return plex;
};
/**
* Creates a close$ Subject for a Plex instance that can be used to manually close the stream or emit errors.
*
* @param {Object} plex - The Plex instance to manage.
* @returns {ReplaySubject} - A ReplaySubject that manages closure and errors and replays once for late subscribers.
*/
export const createClose$ = (plex) => {
if (plex.close$) return plex.close$;
const subject = new ReplaySubject(1);
const stream = plex.mux.stream;
// Attach event listeners to the stream
const errorHandler = (error) => {
if (!subject.closed) {
subject.next(error);
subject.error(error);
}
};
const closeHandler = () => {
if (!subject.closed) {
subject.next();
subject.complete();
}
};
plex.mux.stream.on('error', errorHandler);
plex.mux.stream.on('close', closeHandler);
// Override subject's next and error methods to destroy the stream accordingly
const originalNext = subject.next.bind(subject);
const originalError = subject.error.bind(subject);
subject.next = (value) => {
if (!subject.closed) {
if (value instanceof Error) {
if (!(stream.destroying || stream.destroyed)) plex.mux.stream.destroy(value);
originalNext(value);
originalError(value); // Emit error to subscribers
} else {
if (!(stream.destroying || stream.destroyed)) plex.mux.stream.destroy(value);
originalNext(undefined); // Emit next to indicate normal closure
subject.complete();
}
}
};
subject.error = (err) => {
if (!subject.closed) {
if (!(stream.destroying || stream.destroyed)) plex.mux.stream.destroy(err);
originalError(err);
}
};
// Cleanup event listeners when the Subject is unsubscribed or closed
const cleanup = () => {
plex.mux.stream.off('error', errorHandler);
plex.mux.stream.off('close', closeHandler);
};
subject.subscribe({
complete: cleanup,
error: cleanup,
});
plex.close$ = subject;
return plex.close$;
};