stream-to-it
Version:
Convert Node.js streams to streaming iterables
112 lines • 3.79 kB
JavaScript
/**
* Convert a Node.js [`Writable`](https://nodejs.org/dist/latest/docs/api/stream.html#class-streamwritable)
* stream to a [sink](https://achingbrain.github.io/it-stream-types/interfaces/Sink.html).
*/
export function sink(writable) {
return async (source) => {
const maybeEndSource = async () => {
if (isAsyncGenerator(source)) {
await source.return(undefined);
}
};
let error;
let errCb;
const errorHandler = (err) => {
error = err;
// When the writable errors, try to end the source to exit iteration early
maybeEndSource()
.catch(err => {
err = new AggregateError([
error,
err
], 'The Writable emitted an error, additionally an error occurred while ending the Source');
})
.finally(() => {
errCb?.(err);
});
};
let closeCb;
let closed = false;
const closeHandler = () => {
closed = true;
closeCb?.();
};
let finishCb;
let finished = false;
const finishHandler = () => {
finished = true;
finishCb?.();
};
let drainCb;
const drainHandler = () => {
drainCb?.();
};
const waitForDrainOrClose = async () => {
return new Promise((resolve, reject) => {
closeCb = drainCb = resolve;
errCb = reject;
writable.once('drain', drainHandler);
});
};
const waitForDone = async () => {
// Immediately try to end the source
await maybeEndSource();
return new Promise((resolve, reject) => {
if (closed || finished || (error != null)) {
resolve();
return;
}
finishCb = closeCb = resolve;
errCb = reject;
});
};
const cleanup = () => {
writable.removeListener('error', errorHandler);
writable.removeListener('close', closeHandler);
writable.removeListener('finish', finishHandler);
writable.removeListener('drain', drainHandler);
};
writable.once('error', errorHandler);
writable.once('close', closeHandler);
writable.once('finish', finishHandler);
try {
for await (const value of source) {
if (!writable.writable || writable.destroyed || (error != null)) {
break;
}
if (!writable.write(value)) {
await waitForDrainOrClose();
}
}
}
catch (err) {
// error is set by stream error handler so only destroy stream if source
// threw
if (error == null) {
writable.destroy(err);
}
// could we be obscuring an error here?
error = err;
}
try {
// We're done writing, end everything (n.b. stream may be destroyed at this
// point but then this is a no-op)
if (writable.writable) {
writable.end();
}
// Wait until we close or finish. This supports halfClosed streams
await waitForDone();
// Notify the user an error occurred
if (error != null)
throw error;
}
finally {
// Clean up listeners
cleanup();
}
};
}
function isAsyncGenerator(obj) {
return obj.return != null;
}
//# sourceMappingURL=sink.js.map