2026-07-19 13:44:11 -07:00
|
|
|
import { Readable, Writable, Duplex } from 'stream';
|
|
|
|
|
|
2026-07-20 14:48:28 -07:00
|
|
|
/**
|
|
|
|
|
* stream-to-push-stream - Convert Node.js streams to push-streams
|
|
|
|
|
*
|
|
|
|
|
* Bridges Node.js Readable/Writable/Duplex streams into the push-stream protocol
|
|
|
|
|
* so they can be composed with `@push-stream-std/*` operators.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-07-19 13:44:11 -07:00
|
|
|
class PushSource {
|
|
|
|
|
constructor(readable) {
|
|
|
|
|
this.readable = readable;
|
|
|
|
|
this.sink = null;
|
|
|
|
|
this.paused = true;
|
|
|
|
|
this.ended = false;
|
|
|
|
|
this._endedWithError = null;
|
|
|
|
|
|
|
|
|
|
this._onData = this._onData.bind(this);
|
|
|
|
|
this._onEnd = this._onEnd.bind(this);
|
|
|
|
|
this._onError = this._onError.bind(this);
|
|
|
|
|
this._onResume = this._onResume.bind(this);
|
|
|
|
|
|
|
|
|
|
readable.on('data', this._onData);
|
|
|
|
|
readable.on('end', this._onEnd);
|
|
|
|
|
readable.on('error', this._onError);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pipe(sink) {
|
|
|
|
|
this.sink = sink;
|
|
|
|
|
sink.source = this;
|
|
|
|
|
if (!sink.paused) {
|
|
|
|
|
this.resume();
|
|
|
|
|
}
|
|
|
|
|
return sink;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resume() {
|
|
|
|
|
if (this.ended) return;
|
|
|
|
|
this.paused = false;
|
|
|
|
|
this.readable.resume();
|
|
|
|
|
this._drain();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
abort(err) {
|
|
|
|
|
if (this.ended) return;
|
|
|
|
|
this.ended = err || new Error('Aborted');
|
|
|
|
|
this._cleanup();
|
|
|
|
|
if (this.sink) {
|
|
|
|
|
this.sink.end(this.ended);
|
|
|
|
|
this.sink.source = null;
|
|
|
|
|
this.sink = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_drain() {
|
|
|
|
|
// _drain is no longer needed - _onData handles all data events
|
|
|
|
|
// This method is kept for compatibility but does nothing
|
|
|
|
|
// The readable stream's 'data' event handler (_onData) is already set up in constructor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_onData(data) {
|
|
|
|
|
if (this.ended) return;
|
|
|
|
|
if (this.sink) {
|
|
|
|
|
if (this.sink.paused) {
|
|
|
|
|
this.paused = true;
|
|
|
|
|
this.readable.pause();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.sink.write(data);
|
|
|
|
|
this.paused = this.sink.paused;
|
|
|
|
|
} else {
|
|
|
|
|
this.paused = true;
|
|
|
|
|
this.readable.pause();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_onEnd() {
|
|
|
|
|
if (this.ended) return;
|
|
|
|
|
this.ended = true;
|
|
|
|
|
this._cleanup();
|
|
|
|
|
if (this.sink) {
|
|
|
|
|
this.sink.end(this._endedWithError);
|
|
|
|
|
this.sink.source = null;
|
|
|
|
|
this.sink = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_onError(err) {
|
|
|
|
|
if (this.ended) return;
|
|
|
|
|
this._endedWithError = err;
|
|
|
|
|
this.ended = err;
|
|
|
|
|
this._cleanup();
|
|
|
|
|
if (this.sink) {
|
|
|
|
|
this.sink.end(err);
|
|
|
|
|
this.sink.source = null;
|
|
|
|
|
this.sink = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_onResume() {
|
|
|
|
|
if (this.paused && !this.ended) {
|
|
|
|
|
this.paused = false;
|
|
|
|
|
this.readable.resume();
|
|
|
|
|
this._drain();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_cleanup() {
|
|
|
|
|
this.readable.removeListener('data', this._onData);
|
|
|
|
|
this.readable.removeListener('end', this._onEnd);
|
|
|
|
|
this.readable.removeListener('error', this._onError);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class PushSink {
|
|
|
|
|
constructor(writable) {
|
|
|
|
|
this.writable = writable;
|
|
|
|
|
this.source = null;
|
|
|
|
|
this.paused = false;
|
|
|
|
|
this.ended = false;
|
|
|
|
|
this._endedWithError = null;
|
|
|
|
|
this._draining = false;
|
|
|
|
|
|
|
|
|
|
this._onDrain = this._onDrain.bind(this);
|
|
|
|
|
this._onError = this._onError.bind(this);
|
|
|
|
|
|
|
|
|
|
writable.on('drain', this._onDrain);
|
|
|
|
|
writable.on('error', this._onError);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pipe(sink) {
|
|
|
|
|
if (sink && sink.write) {
|
|
|
|
|
this.source = sink;
|
|
|
|
|
sink.source = this;
|
|
|
|
|
}
|
|
|
|
|
return sink;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
write(data) {
|
|
|
|
|
if (this.ended) return;
|
|
|
|
|
const ok = this.writable.write(data);
|
|
|
|
|
if (!ok) {
|
|
|
|
|
this.paused = true;
|
|
|
|
|
}
|
|
|
|
|
return ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
end(err) {
|
|
|
|
|
if (this.ended) return;
|
|
|
|
|
this.ended = err || true;
|
|
|
|
|
this._cleanup();
|
|
|
|
|
if (this.writable.destroyed || this.writable.writableEnded) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (err) {
|
|
|
|
|
this.writable.destroy(err);
|
|
|
|
|
} else {
|
|
|
|
|
this.writable.end();
|
|
|
|
|
}
|
|
|
|
|
if (this.source) {
|
|
|
|
|
this.source.sink = null;
|
|
|
|
|
this.source = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
abort(err) {
|
|
|
|
|
if (this.ended) return;
|
|
|
|
|
this.ended = err || new Error('Aborted');
|
|
|
|
|
this._cleanup();
|
|
|
|
|
if (!this.writable.destroyed) {
|
|
|
|
|
this.writable.destroy(this.ended);
|
|
|
|
|
}
|
|
|
|
|
if (this.source) {
|
|
|
|
|
this.source.abort(this.ended);
|
|
|
|
|
this.source = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_onDrain() {
|
|
|
|
|
if (this.paused && !this.ended) {
|
|
|
|
|
this.paused = false;
|
|
|
|
|
if (this.source) {
|
|
|
|
|
this.source.resume();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_onError(err) {
|
|
|
|
|
if (this.ended) return;
|
|
|
|
|
this._endedWithError = err;
|
|
|
|
|
this.ended = err;
|
|
|
|
|
this._cleanup();
|
|
|
|
|
if (this.source) {
|
|
|
|
|
this.source.abort(err);
|
|
|
|
|
this.source.sink = null;
|
|
|
|
|
this.source = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_cleanup() {
|
|
|
|
|
this.writable.removeListener('drain', this._onDrain);
|
|
|
|
|
this.writable.removeListener('error', this._onError);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class PushDuplex {
|
|
|
|
|
constructor(duplex) {
|
|
|
|
|
this.source = new PushSource(duplex);
|
|
|
|
|
this.sink = new PushSink(duplex);
|
|
|
|
|
this.paused = false;
|
|
|
|
|
this.ended = false;
|
|
|
|
|
|
|
|
|
|
this.sink.source = this.source;
|
|
|
|
|
this.source.sink = this.sink;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pipe(sink) {
|
|
|
|
|
return this.source.pipe(sink);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resume() {
|
|
|
|
|
this.source.resume();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
abort(err) {
|
|
|
|
|
if (this.ended) return;
|
|
|
|
|
this.ended = err || new Error('Aborted');
|
|
|
|
|
this.source.abort(err);
|
|
|
|
|
this.sink.end(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 14:48:28 -07:00
|
|
|
/**
|
|
|
|
|
* Wrap a Node.js Readable stream as a push-stream source.
|
|
|
|
|
* @param {import('stream').Readable} readableStream
|
|
|
|
|
* @returns {object} A push-stream source
|
|
|
|
|
*/
|
2026-07-19 13:44:11 -07:00
|
|
|
export function source(readableStream) {
|
|
|
|
|
if (!(readableStream instanceof Readable)) {
|
|
|
|
|
throw new Error('Expected a Node.js Readable stream');
|
|
|
|
|
}
|
|
|
|
|
return new PushSource(readableStream);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 14:48:28 -07:00
|
|
|
/**
|
|
|
|
|
* Wrap a Node.js Writable stream as a push-stream sink.
|
|
|
|
|
* @param {import('stream').Writable} writableStream
|
|
|
|
|
* @returns {object} A push-stream sink
|
|
|
|
|
*/
|
2026-07-19 13:44:11 -07:00
|
|
|
export function sink(writableStream) {
|
|
|
|
|
if (!(writableStream instanceof Writable)) {
|
|
|
|
|
throw new Error('Expected a Node.js Writable stream');
|
|
|
|
|
}
|
|
|
|
|
return new PushSink(writableStream);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 14:48:28 -07:00
|
|
|
/**
|
|
|
|
|
* Wrap a Node.js Duplex stream as a push-stream duplex (`{ source, sink }`).
|
|
|
|
|
* @param {import('stream').Duplex} duplexStream
|
|
|
|
|
* @returns {{source: object, sink: object}} Push-stream duplex
|
|
|
|
|
*/
|
2026-07-19 13:44:11 -07:00
|
|
|
export function duplex(duplexStream) {
|
|
|
|
|
if (!(duplexStream instanceof Duplex)) {
|
|
|
|
|
throw new Error('Expected a Node.js Duplex stream');
|
|
|
|
|
}
|
|
|
|
|
return new PushDuplex(duplexStream);
|
|
|
|
|
}
|