@promistream/from-node-stream 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ TODO
4
4
 
5
5
  In Node.js streams, there is a thing called a Duplex stream. This is basically a stream that has an independent readable and writable part, that are not connected together. Like a TCP socket, for example; you send data through the writable half and receive data through the readable half - but the data you receive is not the same data as you sent.
6
6
 
7
- Duplex streams are really just two independent streams that are stuffed into a single object, representing a single resource, like a network socket. And this is how ppstreams treats those - as two independent streams. In Promistreams, there are no Duplex streams, so when converting a Node.js Duplex stream, you will have to do the conversion twice - once for the readable part, and once for the writable part. This can be done with the `duplexRead` and `duplexWrite` methods.
7
+ Duplex streams are really just two independent streams that are stuffed into a single object, representing a single resource, like a network socket. And this is how Promistreams treats those - as two independent streams. In Promistreams, there are no Duplex streams, so when converting a Node.js Duplex stream, you will have to do the conversion twice - once for the readable part, and once for the writable part. This can be done with the `duplexRead` and `duplexWrite` methods.
8
8
 
9
9
  __Why does Promistreams not have Duplex streams?__ Because they're surprisingly tricky to implement. They would make the Promistreams specification a lot more complex, and make certain things impossible; for example, a sink stream (writable stream) in Promistreams can return a Promise to signify when it's completely done receiving data, but a source stream (readable stream) needs to return a Promise with the next read value! You can only return one or the other, so supporting both would involve a lot of special logic for detecting what kind of thing is being returned.
10
10
 
package/package.json CHANGED
@@ -1,28 +1,36 @@
1
1
  {
2
2
  "name": "@promistream/from-node-stream",
3
- "version": "0.1.3",
4
- "main": "index.js",
3
+ "version": "0.1.4",
4
+ "main": "src/index.js",
5
5
  "repository": "http://git.cryto.net/promistream/from-node-stream.git",
6
6
  "author": "Sven Slootweg <admin@cryto.net>",
7
7
  "license": "WTFPL OR CC0-1.0",
8
8
  "dependencies": {
9
+ "@joepie91/consumable": "^1.0.1",
9
10
  "@joepie91/unreachable": "^1.0.0",
10
- "@promistream/buffer": "^0.1.0",
11
- "@promistream/end-of-stream": "^0.1.0",
12
- "@promistream/is-end-of-stream": "^0.1.0",
13
- "@promistream/pipe": "^0.1.1",
14
- "@promistream/propagate-abort": "^0.1.3",
15
- "@promistream/propagate-peek": "^0.1.0",
16
- "@promistream/simple-sink": "^0.2.0",
17
- "@promistream/simple-source": "^0.1.1",
18
- "bluebird": "^3.7.2",
19
- "debug": "^4.3.1",
20
- "p-event": "^4.2.0",
11
+ "@joepie91/warning": "^1.1.0",
12
+ "@promistream/end-of-stream": "^0.1.2",
13
+ "@promistream/is-aborted": "^0.1.1",
14
+ "@promistream/is-end-of-stream": "^0.1.1",
15
+ "@promistream/propagate-abort": "^0.1.7",
16
+ "@promistream/propagate-peek": "^0.1.1",
17
+ "@promistream/simple-sink": "^0.3.2",
18
+ "@promistream/simple-source": "^0.1.5",
19
+ "debug-instanced": "^1.0.0",
20
+ "promisify-event": "^1.0.0",
21
+ "push-buffer": "^1.5.5",
22
+ "single-concurrent": "^1.0.0",
21
23
  "split-filter": "^1.1.3"
22
24
  },
23
25
  "devDependencies": {
24
- "@promistream/buffered-map": "^0.1.0",
25
- "@promistream/collect": "^0.1.1",
26
- "@promistream/range-numbers": "^0.1.2"
26
+ "@promistream/buffered-map": "^0.1.1",
27
+ "@promistream/collect": "^0.1.2",
28
+ "@promistream/debug": "^0.1.0",
29
+ "@promistream/from-iterable": "^0.1.0",
30
+ "@promistream/map": "^0.1.5",
31
+ "@promistream/pipe": "^0.1.6",
32
+ "@promistream/range-numbers": "^0.1.2",
33
+ "bluebird": "^3.7.2",
34
+ "through2": "^4.0.2"
27
35
  }
28
36
  }
package/src/index.js ADDED
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+
3
+ const simpleSink = require("@promistream/simple-sink");
4
+ const simpleSource = require("@promistream/simple-source");
5
+ const propagateAbort = require("@promistream/propagate-abort");
6
+ const propagatePeek = require("@promistream/propagate-peek");
7
+ const EndOfStream = require("@promistream/end-of-stream");
8
+ const isEndOfStream = require("@promistream/is-end-of-stream");
9
+ const isAborted = require("@promistream/is-aborted");
10
+ const makeDebug = require("debug-instanced")("promistream:from-node-stream");
11
+
12
+ const pushBuffer = require("push-buffer");
13
+ const promisifyEvent = require("promisify-event");
14
+ const singleConcurrent = require("single-concurrent");
15
+ const warning = require("@joepie91/warning");
16
+ const util = require("node:util");
17
+
18
+ const isStdioStream = require("./is-stdio-stream");
19
+
20
+ function ensureValidError(error) {
21
+ if (error instanceof Error) {
22
+ return error;
23
+ } else {
24
+ let validError = new Error(`Node stream emitted an error value that is not of Error type. Value was: ${util.inspect(error)}`);
25
+ validError.originalValue = error;
26
+ return validError;
27
+ }
28
+ }
29
+
30
+ function isReadableStreamEnded(stream) {
31
+ // FIXME: Are these the correct properties to look at, for a Readable interface?
32
+ return (stream.readableEnded || stream.destroyed);
33
+ }
34
+
35
+ function wireReadable(debug, stream) {
36
+ let buffer = pushBuffer({ mode: "push" });
37
+ let isPaused = false;
38
+ let abortSeen = false;
39
+
40
+ stream.on("data", (value) => {
41
+ debug("Received value from underlying Readable stream");
42
+ buffer.push(value);
43
+
44
+ // We have more values buffered than there are outstanding requests
45
+ if (buffer.countLane().values > 0) {
46
+ debug("Pausing underlying Readable stream; enough values in our buffer already");
47
+
48
+ if (stream.pause != null) {
49
+ isPaused = true;
50
+ stream.pause();
51
+ } else {
52
+ warning("The stream you are converting does not support pausing. This may lead to unexpectedly high memory usage!");
53
+ }
54
+ }
55
+ });
56
+
57
+ stream.on("error", (error) => {
58
+ if (!abortSeen) {
59
+ debug(`Underlying Readable stream produced an error: ${error.message}`);
60
+ abortSeen = true;
61
+ buffer.pushError(ensureValidError(error));
62
+ }
63
+ });
64
+
65
+ stream.on("end", () => {
66
+ debug("Reached end of underlying Readable stream");
67
+ buffer.pushError(new EndOfStream("The underlying stream has ended"));
68
+ });
69
+
70
+ return {
71
+ buffer: buffer,
72
+ read: async function () {
73
+ let promise = buffer.request();
74
+
75
+ // We have run out of values
76
+ if (isPaused && buffer.countLane().requests > 0) {
77
+ debug("Resuming underlying Readable stream, as our buffer ran out");
78
+ stream.resume();
79
+ }
80
+
81
+ return promise;
82
+ },
83
+ abort: async function (error) {
84
+ if (!isStdioStream(stream) && !isReadableStreamEnded(stream)) {
85
+ debug("Aborting underlying Readable stream via .destroy...");
86
+ abortSeen = true;
87
+ stream.destroy(error);
88
+ }
89
+ }
90
+ };
91
+ }
92
+
93
+ function convertFromReadable(stream) {
94
+ /* Node Readable -> Promistream source
95
+ * let Readable emit data events and buffer them up on Promistream side,
96
+ * pausing the Node side if necessary
97
+ */
98
+
99
+ let debug = makeDebug("readable");
100
+
101
+ let { read, abort } = wireReadable(debug, stream);
102
+
103
+ return simpleSource({
104
+ onRequest: () => {
105
+ return read();
106
+ },
107
+ onAbort: () => {
108
+ return abort();
109
+ }
110
+ });
111
+ }
112
+
113
+ function isWritableStreamEnded(stream) {
114
+ // TODO: Is this correct? In the old version, we were looking at stream.closed instead. Original comment related to that:
115
+ // > When a writable stream has emitted its 'close' event, it no longer produces errors and will behave as if it's perpetually backlogged
116
+ // > Ref: https://github.com/nodejs/node/issues/53512
117
+ return (stream.writableEnded || stream.destroyed);
118
+ }
119
+
120
+ function wireWritable(debug, stream) {
121
+ let errorEncountered;
122
+
123
+ stream.on("error", (error) => {
124
+ debug("Logging writable interface error:", error);
125
+ errorEncountered = ensureValidError(error);
126
+ });
127
+
128
+ // We're not wiring into the `finish` event here because we detect the ended stream on the next write attempt instead (inside of `tryWrite`).
129
+
130
+ return {
131
+ destroy: async function (error) {
132
+ // TODO: Do we need to detach any event handlers prior to destroying streams? ref. https://www.npmjs.com/package/yauzl:
133
+ // "You must unpipe() the readStream from any destination before calling readStream.destroy()."
134
+ // TODO: In a previous version, we skipped the .destroy step if the stream didn't have a .destroy method. What was this for? Streams v1?
135
+ if (!isStdioStream(stream) && !isWritableStreamEnded(stream)) {
136
+ debug("Calling .destroy on Writable stream");
137
+ stream.destroy(error);
138
+ }
139
+ },
140
+ end: async function () {
141
+ if (!isStdioStream(stream) && !isWritableStreamEnded(stream)) {
142
+ debug("Calling .end on Writable stream");
143
+ stream.end();
144
+ }
145
+ },
146
+ tryWrite: async function (value, abort) {
147
+ if (isWritableStreamEnded(stream)) {
148
+ debug("Could not write because underlying stream has ended");
149
+ if (errorEncountered != null) {
150
+ debug("... due to an error");
151
+ abort(errorEncountered);
152
+ } else {
153
+ debug("... due to its completion");
154
+ abort(true);
155
+ }
156
+ } else {
157
+ // TODO: Old version had a special case for stdio streams, using the flush callback argument to .write to know when the next write could be done; because according to the stream-to-pull-stream package's code, the `drain` event does not work correctly there. Need to investigate whether this is still true!
158
+ // FIXME: Validate `value`. When the target of the write is process.stdout (presumably any text stream), writing `undefined` is not allowed. May not be allowed for object streams either; need to investigate. This should raise an explicit and clear error.
159
+ let canWriteMore = stream.write(value);
160
+
161
+ if (!canWriteMore) {
162
+ debug("Stream is backlogged, waiting for drain event...");
163
+ await promisifyEvent(stream, "drain");
164
+ debug("Drain event received");
165
+ } else {
166
+ debug("Write successful, can write more");
167
+ }
168
+ }
169
+ }
170
+ };
171
+ }
172
+
173
+ function convertFromWritable(stream) {
174
+ /* Node Writable -> Promistream sink
175
+ * Promistream side continuously requests reads until Node side is corked
176
+ */
177
+
178
+ let debug = makeDebug("writable");
179
+
180
+ let { destroy, end, tryWrite } = wireWritable(debug, stream);
181
+
182
+ return simpleSink({
183
+ onAbort: async (error) => {
184
+ if (error === true) {
185
+ return end();
186
+ } else {
187
+ return destroy(error);
188
+ }
189
+ },
190
+ onEnd: async () => {
191
+ return end();
192
+ },
193
+ onValue: async (value, abort) => {
194
+ return tryWrite(value, abort);
195
+ }
196
+ });
197
+ }
198
+
199
+ function convertFromTransform(stream) {
200
+ let debug = makeDebug("transform");
201
+
202
+ let { buffer, read } = wireReadable(debug, stream);
203
+ let { destroy, end, tryWrite } = wireWritable(debug, stream);
204
+
205
+ /* while the promistream upstream has not been ended or aborted, keep reading from it and writing into tryWrite (relying on it to provide backpressure via promises). make sure that an error propagates on the next read (insert into read buffer). meanwhile for every own-promistream read request, schedule a buffer read on the readable interface. */
206
+
207
+ let lastSource, endMarker;
208
+
209
+ let tryFeedStream = singleConcurrent(async () => {
210
+ // This is essentially a 'background process' of sorts, with manual error handling wiring.
211
+ try {
212
+ while (endMarker == null) {
213
+ await tryWrite(await lastSource.read());
214
+ }
215
+ } catch (error) {
216
+ if (endMarker == null && (isEndOfStream(error) || isAborted(error))) {
217
+ endMarker = error;
218
+
219
+ // TODO: Is this necessary? Is this not already handled by wiring for the readable/writable interface?
220
+ if (isAborted(error)) {
221
+ if (error.cause instanceof Error) {
222
+ destroy(error);
223
+ } else {
224
+ // Normally ended
225
+ end();
226
+ }
227
+ } else if (isEndOfStream(error)) {
228
+ end();
229
+ }
230
+ }
231
+
232
+ buffer.pushError(error);
233
+ }
234
+ });
235
+
236
+ return {
237
+ _promistreamVersion: 0,
238
+ description: `Node.js transform stream`,
239
+ abort: propagateAbort,
240
+ peek: propagatePeek,
241
+ read: async function produceValue_transformStream(source) {
242
+ lastSource = source;
243
+ tryFeedStream();
244
+ return read();
245
+ }
246
+ };
247
+ }
248
+
249
+ function convert(stream) {
250
+ // FIXME: Proper validation and tagging?
251
+ // TODO: Wrap v1 streams to support them
252
+ // NOTE: Standard I/O streams are specialcased here because they may be Duplex streams; even though the other half is never actually used. We're only interested in the interface that *is* being used.
253
+ if (stream === process.stdin) {
254
+ return convertFromReadable(stream);
255
+ } else if (stream === process.stdout || stream === process.stderr) {
256
+ return convertFromWritable(stream);
257
+ } else if (stream.writable != null) {
258
+ if (stream.readable != null) {
259
+ if (stream._transform != null) {
260
+ // transform
261
+ return convertFromTransform(stream);
262
+ } else {
263
+ throw new Error(`Duplex streams cannot be converted with the auto-detection API. Instead, use 'fromReadable' and/or 'fromWritable' manually, depending on which parts of the Duplex stream you are interested in.`);
264
+ }
265
+ } else {
266
+ return convertFromWritable(stream);
267
+ }
268
+ } else if (stream.readable != null) {
269
+ return convertFromReadable(stream);
270
+ } else {
271
+ throw new Error(`Not a Node stream`);
272
+ }
273
+ }
274
+
275
+ module.exports = convert;
276
+ module.exports.fromReadable = convertFromReadable;
277
+ module.exports.fromWritable = convertFromWritable;
278
+ module.exports.fromTransform = convertFromTransform;
package/test.js ADDED
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+
3
+ const test = require("node:test");
4
+ const assert = require("node:assert");
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ const stream = require("node:stream");
8
+
9
+ const pipe = require("@promistream/pipe");
10
+ const fromIterable = require("@promistream/from-iterable");
11
+ const fromNodeStream = require(".");
12
+ const collect = require("@promistream/collect");
13
+ const debug = require("@promistream/debug");
14
+
15
+ test("readable stream", async () => {
16
+ let testFile = path.join(__dirname, "pnpm-lock.yaml"); // Big-ish file, so we're likely to read in multiple chunks
17
+ let expected = await fs.promises.readFile(testFile);
18
+
19
+ let result = await pipe([
20
+ fromNodeStream.fromReadable(fs.createReadStream(testFile)),
21
+ collect()
22
+ ]).read();
23
+
24
+ assert.deepStrictEqual(result, [ expected ]);
25
+ });
26
+
27
+ test("writable stream", async () => {
28
+ let buffer = Buffer.alloc(400000);
29
+
30
+ let writableBuffer = Buffer.alloc(0);
31
+ let endReached = false;
32
+ let nodeWritable = new stream.Writable({
33
+ write(chunk, _encoding, callback) {
34
+ writableBuffer = Buffer.concat([ writableBuffer, chunk ]);
35
+ callback();
36
+ },
37
+ final(callback) {
38
+ endReached = true;
39
+ callback();
40
+ }
41
+ });
42
+
43
+ await pipe([
44
+ fromIterable([ buffer, buffer, buffer ]),
45
+ fromNodeStream(nodeWritable)
46
+ ]).read();
47
+
48
+ assert.strictEqual(writableBuffer.length, 400000 * 3);
49
+ assert.strictEqual(endReached, true);
50
+ });
51
+
52
+ test("transform stream", async () => {
53
+ let result = await pipe([
54
+ fromIterable([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]),
55
+ fromNodeStream(new stream.Transform({
56
+ objectMode: true,
57
+ transform(chunk, _encoding, callback) {
58
+ this.push(chunk);
59
+ this.push(chunk * 2);
60
+ callback();
61
+ }
62
+ })),
63
+ collect()
64
+ ]).read();
65
+
66
+ assert.deepStrictEqual(result, [ 0, 0, 1, 2, 2, 4, 3, 6, 4, 8, 5, 10, 6, 12, 7, 14, 8, 16, 9, 18, 10, 20 ]);
67
+ });
package/index.js DELETED
@@ -1,178 +0,0 @@
1
- "use strict";
2
-
3
- const Promise = require("bluebird");
4
- const simpleSource = require("@promistream/simple-source");
5
- const simpleSink = require("@promistream/simple-sink");
6
- const buffer = require("@promistream/buffer");
7
- const propagatePeek = require("@promistream/propagate-peek");
8
- const propagateAbort = require("@promistream/propagate-abort");
9
- const pipe = require("@promistream/pipe");
10
- const isEndOfStream = require("@promistream/is-end-of-stream");
11
- const debug = require("debug");
12
-
13
- const createDefer = require("./src/create-defer");
14
- const wireUpReadableInterface = require("./src/readable");
15
- const wireUpWritableInterface = require("./src/writable");
16
-
17
- // FIXME: Maybe also an abstraction for 'handle queue of requests', as this is used in multiple stream implementations
18
- // TODO: Improve robustness of stream-end handling using https://nodejs.org/dist/latest-v14.x/docs/api/stream.html#stream_stream_finished_stream_options_callback?
19
- // FIXME: Sequentialize all of these?
20
-
21
- // readable
22
- // writable
23
- // transform
24
- // duplex
25
-
26
- module.exports = function convert(stream) {
27
- // FIXME: Proper validation and tagging
28
- // FIXME: Wrap v1 streams
29
- // NOTE: Standard I/O streams are specialcased here because they may be Duplex streams; even though the other half is never actually used. We're only interested in the interface that *is* being used.
30
- if (stream === process.stdin) {
31
- return fromReadable(stream);
32
- } else if (stream === process.stdout || stream === process.stderr) {
33
- return fromWritable(stream);
34
- } else if (stream.writable != null) {
35
- if (stream.readable != null) {
36
- if (stream._transform != null) {
37
- // transform
38
- return fromTransform(stream);
39
- } else {
40
- throw new Error(`Duplex streams cannot be converted with the auto-detection API. Instead, use 'fromReadable' and/or 'fromWritable' manually, depending on which parts of the Duplex stream you are interested in.`);
41
- }
42
- } else {
43
- return fromWritable(stream);
44
- }
45
- } else if (stream.readable != null) {
46
- return fromReadable(stream);
47
- } else {
48
- throw new Error(`Not a Node stream`);
49
- }
50
- };
51
-
52
- function fromReadable(stream) {
53
- let readable = wireUpReadableInterface(stream);
54
-
55
- return simpleSource({
56
- onRequest: () => {
57
- return readable.request();
58
- },
59
- onAbort: () => {
60
- return readable.destroy();
61
- }
62
- });
63
- }
64
-
65
- let debugWritable = debug("promistream:from-node-stream:writable");
66
-
67
- function fromWritable(stream) {
68
- let upstreamHasEnded = false;
69
- let mostRecentSource = { abort: function() {} }; // FIXME: Replace with a proper spec-compliant dummy stream
70
-
71
- let convertedStream = simpleSink({
72
- onResult: (result) => {
73
- debugWritable("Received value");
74
- return writable.write(result);
75
- },
76
- onEnd: () => {
77
- debugWritable("Upstream reported end-of-stream");
78
- upstreamHasEnded = true;
79
- return writable.end();
80
- },
81
- onAbort: (_reason) => {
82
- debugWritable("Pipeline was aborted");
83
- return writable.destroy();
84
- },
85
- onSourceChanged: (source) => {
86
- debugWritable("A source change occurred");
87
- mostRecentSource = source;
88
- }
89
- });
90
-
91
- // NOTE: The use of `var` is intentional, to make hoisting possible here; otherwise we'd have a broken cyclical reference
92
- var writable = wireUpWritableInterface(stream, {
93
- onEnd: () => {
94
- debugWritable(`Underlying stream has reported a close event (upstreamHasEnded = ${upstreamHasEnded})`);
95
-
96
- if (!upstreamHasEnded) {
97
- debugWritable(`Issuing happy abort on converted stream`);
98
- convertedStream.abort(true, mostRecentSource);
99
- }
100
- },
101
- onError: (error) => {
102
- // Make sure we notify the pipeline, if any, by passing in the most recent source stream that we've seen.
103
- debugWritable(`Issuing error abort on converted stream due to: ${error.message}`);
104
- convertedStream.abort(error, mostRecentSource);
105
- }
106
- });
107
-
108
- return convertedStream;
109
- }
110
-
111
- let debugTransform = debug("promistream:from-node-stream:transform");
112
-
113
- function fromTransform(stream) {
114
- let endHandled = false;
115
-
116
- // FIXME: we need to specifically watch for the `error` and `end` events on the readable interface, to know when the transform stream has fully completed processing
117
- // Respond to the EndOfStream produced by the pushbuffer in this case
118
- // request, destroy
119
- let readable = wireUpReadableInterface(stream, {
120
- onEnd: () => {
121
- debugTransform("Received end/close event from underlying stream");
122
- },
123
- onError: () => {
124
- debugTransform("Received error event from underlying stream");
125
- }
126
- });
127
-
128
- // write, end, destroy
129
- let writable = wireUpWritableInterface(stream);
130
-
131
- let convertedStream = {
132
- _promistreamVersion: 0,
133
- description: `converted Node.js transform stream`,
134
- abort: propagateAbort,
135
- peek: propagatePeek,
136
- read: function produceValue_nodeTransformStream(source) {
137
- if (endHandled) {
138
- // NOTE: This logic exists at the start, not in the upstream EndOfStream handling code, because any number of buffer reads may be required before the wrapped Node stream can be closed
139
- // NOTE: The push-buffer will automatically produce EndOfStream markers once the buffer has run out and the underlying stream has closed, so long as we're using the wireUpReadableInterface function
140
- // FIXME: Refactor this design to request-matcher instead?
141
- return Promise.try(() => {
142
- return readable.request();
143
- }).then((result) => {
144
- return [ result ];
145
- });
146
- } else {
147
- return Promise.try(() => {
148
- debugTransform("Doing upstream read...");
149
- return source.read();
150
- }).then((value) => {
151
- debugTransform("Writing upstream value to writable interface");
152
- writable.write(value);
153
-
154
- // This will quite possibly return an empty buffer, but that is fine; the `buffer` stream downstream from us will just keep reading (and therefore queueing up new items to be transformed) until it gets some results.
155
- debugTransform("Consuming immediate buffer from readable interface");
156
- return readable.consumeImmediateBuffer();
157
- }).catch(isEndOfStream, () => {
158
- debugTransform("End of upstream reached");
159
- endHandled = true;
160
-
161
- debugTransform("Closing via writable interface");
162
- writable.end();
163
-
164
- // Return nothing, let the next read call (and all of those after that) deal with either underlying stream completion or buffered results
165
- return [];
166
- });
167
- }
168
- }
169
- };
170
-
171
- return pipe([
172
- convertedStream,
173
- buffer()
174
- ]);
175
- }
176
-
177
- module.exports.fromReadable = fromReadable;
178
- module.exports.fromWritable = fromWritable;
@@ -1,16 +0,0 @@
1
- "use strict";
2
-
3
- const util = require("util");
4
-
5
- module.exports = function assertErrorType(error) {
6
- if (!(error instanceof Error)) {
7
- let stack = (new Error).stack
8
- .split("\n")
9
- .filter((line, n) => (n > 0 || !line.startsWith("Error")))
10
- .join("\n");
11
-
12
- console.warn(`Converted Node stream produced an error value that isn't an Error instance: ${util.inspect(error)}`);
13
- console.warn(`Stack:`);
14
- console.warn(stack);
15
- }
16
- };
@@ -1,20 +0,0 @@
1
- "use strict";
2
-
3
- // FIXME: Can we use p-defer here instead?
4
- module.exports = function createDefer() {
5
- let resolveFunc, rejectFunc;
6
-
7
- // NOTE: This works because the `new Promise` callback gets executed synchronously.
8
- let promise = new Promise((resolve, reject) => {
9
- resolveFunc = resolve;
10
- rejectFunc = reject;
11
- });
12
-
13
- return {
14
- promise: promise,
15
- defer: {
16
- resolve: resolveFunc,
17
- reject: rejectFunc
18
- }
19
- };
20
- };
@@ -1,11 +0,0 @@
1
- "use strict";
2
-
3
- const warn = require("./warn");
4
-
5
- module.exports = function destroyStream(stream) {
6
- if (typeof stream.destroy === "function") {
7
- stream.destroy();
8
- } else {
9
- warn("The stream you are converting does not have a 'destroy' method, and could therefore not be destroyed. This may cause resource leaks!");
10
- }
11
- };
@@ -1,35 +0,0 @@
1
- "use strict";
2
-
3
- const debug = require("debug")("promistream:from-node-stream:readable:attach-handlers");
4
-
5
- module.exports = function attachReadableStreamHandlers({ stream, onClose, onError, onData }) {
6
- function detachEventHandlers() {
7
- debug("Detaching event handlers");
8
- stream.removeListener("end", onCloseWrapper);
9
- stream.removeListener("close", onCloseWrapper);
10
- stream.removeListener("error", onErrorWrapper);
11
- stream.removeListener("data", onData);
12
- }
13
-
14
- function attachEventHandlers() {
15
- debug("Attaching event handlers");
16
- stream.on("end", onCloseWrapper);
17
- stream.on("close", onCloseWrapper);
18
- stream.on("error", onErrorWrapper);
19
- stream.on("data", onData);
20
- }
21
-
22
- function onCloseWrapper() {
23
- debug("onCloseWrapper called");
24
- onClose();
25
- detachEventHandlers();
26
- }
27
-
28
- function onErrorWrapper(error) {
29
- debug("onErrorWrapper called");
30
- onError(error);
31
- detachEventHandlers();
32
- }
33
-
34
- attachEventHandlers();
35
- };
@@ -1,68 +0,0 @@
1
- "use strict";
2
-
3
- const debug = require("debug")("promistream:from-node-stream:readable");
4
-
5
- const attachHandlers = require("./attach-handlers");
6
- const createPushBuffer = require("./push-buffer");
7
- const destroyStream = require("../destroy-stream");
8
- const assertErrorType = require("../assert-error-type");
9
-
10
- module.exports = function wireUpReadableInterface(stream, { onEnd, onError } = {}) {
11
- let pushBuffer = createPushBuffer({
12
- onPause: function () {
13
- if (stream.pause != null) {
14
- debug("Pausing underlying stream");
15
- stream.pause();
16
- return true; // FIXME: Can we verify whether the pausing was successful, somehow? Eg. to deal with streams with `readable` event handlers attached.
17
- } else {
18
- return false;
19
- }
20
- },
21
- onResume: function () {
22
- if (stream.resume != null) {
23
- debug("Resuming underlying stream");
24
- stream.resume();
25
- return true;
26
- } else {
27
- throw new Error(`Stream was successfully paused but does not have a resume method. This should never happen!`);
28
- }
29
- }
30
- });
31
-
32
- // TODO: Verify that auto-detaching all event handlers upon end/error is actually the correct thing to do!
33
- attachHandlers({
34
- stream: stream,
35
- onData: (data) => {
36
- if (Buffer.isBuffer(data)) {
37
- debug(`Chunk emitted of length ${data.length}`);
38
- } else {
39
- debug(`Value emitted`);
40
- }
41
-
42
- pushBuffer.queueValue(data);
43
- },
44
- onError: (error) => {
45
- assertErrorType(error);
46
- pushBuffer.queueError(error);
47
-
48
- if (onError != null) {
49
- onError(error);
50
- }
51
- },
52
- onClose: () => {
53
- pushBuffer.markEnded();
54
-
55
- if (onEnd != null) {
56
- onEnd();
57
- }
58
- }
59
- });
60
-
61
- return {
62
- request: pushBuffer.queueRequest,
63
- consumeImmediateBuffer: pushBuffer.consumeImmediateBuffer,
64
- destroy: () => {
65
- return destroyStream(stream);
66
- }
67
- };
68
- };
@@ -1,143 +0,0 @@
1
- "use strict";
2
-
3
- // FIXME: Separate this out into its own package
4
-
5
- const splitFilter = require("split-filter");
6
- const unreachable = require("@joepie91/unreachable")("@promistream/from-node-stream");
7
- const EndOfStream = require("@promistream/end-of-stream");
8
- const debug = require("debug")("promistream:from-node-stream:push-buffer");
9
-
10
- const warn = require("../warn");
11
- const createDefer = require("../create-defer");
12
-
13
- module.exports = function createPushBuffer(options) {
14
- let onPause = options.onPause || function pauseNotImplemented() {
15
- return false;
16
- };
17
-
18
- let onResume = options.onResume || function resumeNotImplemented() {
19
- return false;
20
- };
21
-
22
- // TODO: Use @joepie91/consumable here?
23
- let itemBuffer = [];
24
- let requestQueue = [];
25
- let isPaused = false;
26
- let hasEnded = false;
27
-
28
- function resumeIfEmpty() {
29
- let bufferIsEmpty = (itemBuffer.length === 0);
30
-
31
- if (bufferIsEmpty && isPaused) {
32
- if (onResume() === true) {
33
- isPaused = false;
34
- }
35
- }
36
- }
37
-
38
- function attemptDrain() {
39
- // NOTE: This must remain fully synchronous, if we want to avoid unnecessary pauses in the `data` handler
40
- debug("Drain attempt started");
41
-
42
- if (requestQueue.length > 0) {
43
- while (requestQueue.length > 0) {
44
- let hasItems = (itemBuffer.length > 0);
45
- let hasResponse = (hasEnded || hasItems);
46
-
47
- if (hasResponse) {
48
- debug("Satisfying queued request");
49
- let defer = requestQueue.shift();
50
-
51
- if (hasItems) {
52
- // FIXME: Does this correctly deal with an error event produced as a result of an abort?
53
- let item = itemBuffer.shift();
54
-
55
- if (item.type === "value") {
56
- defer.resolve(item.value);
57
- } else if (item.type === "error") {
58
- defer.reject(item.error);
59
- } else {
60
- unreachable(`Unexpected item type '${item.type}'`);
61
- }
62
- } else if (hasEnded) {
63
- defer.reject(new EndOfStream());
64
- } else {
65
- unreachable("Invalid response state, neither has items in queue nor ended");
66
- }
67
- } else {
68
- debug("No data available to satisfy queued request");
69
- break;
70
- }
71
- }
72
- } else {
73
- debug("No outstanding requests to satisfy");
74
- }
75
-
76
- resumeIfEmpty();
77
- }
78
-
79
- return {
80
- queueValue: function (value) {
81
- debug("Queueing value");
82
- itemBuffer.push({ type: "value", value: value });
83
- attemptDrain();
84
-
85
- let stillHasItemsBuffered = (itemBuffer.length > 0);
86
-
87
- if (stillHasItemsBuffered && !isPaused) {
88
- if (onPause() === true) {
89
- isPaused = true;
90
- } else {
91
- // FIXME: Only show this warning once?
92
- warn("The stream you are converting does not support pausing. This may lead to unexpectedly high memory usage!");
93
- }
94
- }
95
- },
96
- queueError: function (error) {
97
- debug("Queueing error");
98
- itemBuffer.push({ type: "error", error: error });
99
- attemptDrain();
100
- },
101
- queueRequest: function () {
102
- debug("Queueing read request");
103
- let { defer, promise } = createDefer();
104
- requestQueue.push(defer);
105
- attemptDrain();
106
- return promise;
107
- },
108
- markEnded: function () {
109
- debug("Marking as ended");
110
- hasEnded = true;
111
- attemptDrain();
112
- },
113
- consumeImmediateBuffer: function () {
114
- debug("Post-drain remaining buffer requested");
115
- attemptDrain();
116
-
117
- debug("Returning immediate buffer");
118
-
119
- // FIXME: Only return successful items here?
120
- if (requestQueue.length > 0) {
121
- // We won't ever serve up the buffer until any individual-item requests have been fulfilled.
122
- return [];
123
- } else {
124
- let [ values, errors ] = splitFilter(itemBuffer, (item) => item.type === "value");
125
-
126
- debug(`Buffer contains ${errors.length} errors and ${values.length} values`);
127
-
128
- if (errors.length > 0) {
129
- debug("Throwing first error");
130
-
131
- itemBuffer = values; // In case we ever write code that will do something with the remaining values in the buffer
132
- throw errors[0].error;
133
- } else {
134
- debug(`Returning ${values.length} values`);
135
-
136
- itemBuffer = [];
137
- resumeIfEmpty(); // Ensure that we haven't left the source stream in a paused state, because that would deadlock the pipeline
138
- return values.map((item) => item.value);
139
- }
140
- }
141
- }
142
- };
143
- };
package/src/warn.js DELETED
@@ -1,5 +0,0 @@
1
- "use strict";
2
-
3
- module.exports = function warn(message) {
4
- console.error(`[@promistream/from-node-stream] Warning: ${message}`);
5
- };
@@ -1,27 +0,0 @@
1
- "use strict";
2
-
3
- module.exports = function attachWritableStreamHandlers({ stream, onClose, onError }) {
4
- function detachEventHandlers() {
5
- stream.removeListener("finish", onCloseWrapper);
6
- stream.removeListener("close", onCloseWrapper);
7
- stream.removeListener("error", onErrorWrapper);
8
- }
9
-
10
- function attachEventHandlers() {
11
- stream.on("finish", onCloseWrapper);
12
- stream.on("close", onCloseWrapper);
13
- stream.on("error", onErrorWrapper);
14
- }
15
-
16
- function onCloseWrapper() {
17
- onClose();
18
- detachEventHandlers();
19
- }
20
-
21
- function onErrorWrapper(error) {
22
- onError(error);
23
- detachEventHandlers();
24
- }
25
-
26
- attachEventHandlers();
27
- };
@@ -1,49 +0,0 @@
1
- "use strict";
2
-
3
- const pEvent = require("p-event");
4
- const debug = require("debug")("promistream:from-node-stream:writable");
5
-
6
- const attachHandlers = require("./attach-handlers");
7
- const writeToStream = require("./write-to-stream");
8
- const isStdioStream = require("../is-stdio-stream");
9
- const destroyStream = require("../destroy-stream");
10
- const assertErrorType = require("../assert-error-type");
11
-
12
- module.exports = function wireUpWritableInterface(stream, { onEnd, onError } = {}) {
13
- attachHandlers({
14
- stream: stream,
15
- onClose: () => {
16
- if (onEnd != null) {
17
- onEnd();
18
- }
19
- },
20
- onError: (error) => {
21
- assertErrorType(error);
22
-
23
- if (onError != null) {
24
- onError(error);
25
- }
26
- }
27
- });
28
-
29
- return {
30
- write: function (value) {
31
- return writeToStream(stream, value);
32
- },
33
- end: function () {
34
- // stdout/stderr cannot be ended like other streams
35
- if (!isStdioStream(stream)) {
36
- debug("Ending stream");
37
-
38
- let finishPromise = pEvent(stream, "finish");
39
- stream.end();
40
- return finishPromise;
41
- } else {
42
- debug("Not ending stream because it is stdio");
43
- }
44
- },
45
- destroy: function () {
46
- return destroyStream(stream);
47
- }
48
- };
49
- };
@@ -1,33 +0,0 @@
1
- "use strict";
2
-
3
- const debug = require("debug")("promistream:from-node-stream:writable");
4
-
5
- const isStdioStream = require("../is-stdio-stream");
6
-
7
- module.exports = function writeToStream(stream, value) {
8
- if (!isStdioStream(stream)) {
9
- let canWriteMore = stream.write(value);
10
-
11
- if (canWriteMore) {
12
- debug("Stream can accept more data");
13
- return;
14
- } else {
15
- debug("Stream is backed up, waiting for drain event...");
16
- // TODO: Use p-event instead?
17
- return new Promise((resolve, _reject) => {
18
- stream.once("drain", () => {
19
- debug("Drain event received");
20
- resolve();
21
- });
22
- });
23
- }
24
- } else {
25
- // NOTE: According to the `stream-to-pull-stream` code, stdout/stderr behave differently from normal streams, and the `drain` event doesn't work correctly there. So instead, we use the flush callback to know when to write the next bit of data.
26
- return new Promise((resolve, _reject) => {
27
- stream.write(value, (_error) => {
28
- // NOTE: We ignore any errors here, and wait for them to be thrown in the `error` event, to simplify the logic.
29
- resolve();
30
- });
31
- });
32
- }
33
- };