@promistream/from-node-stream 0.1.8 → 0.1.10
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/package.json +2 -1
- package/src/index.js +40 -9
- package/test.js +119 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promistream/from-node-stream",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"@promistream/parallelize": "^0.1.10",
|
|
34
34
|
"@promistream/pipe": "^0.1.8",
|
|
35
35
|
"@promistream/range-numbers": "^0.1.2",
|
|
36
|
+
"@promistream/spy": "^0.1.0",
|
|
36
37
|
"@promistream/test-abort": "^0.1.0",
|
|
37
38
|
"bluebird": "^3.7.2",
|
|
38
39
|
"p-defer": "^4.0.1",
|
package/src/index.js
CHANGED
|
@@ -36,6 +36,12 @@ function wireReadable(debug, stream) {
|
|
|
36
36
|
let buffer = pushBuffer({ mode: "push" });
|
|
37
37
|
let isPaused = false;
|
|
38
38
|
let abortSeen = false;
|
|
39
|
+
let endError;
|
|
40
|
+
|
|
41
|
+
function finishStream(error) {
|
|
42
|
+
endError = error;
|
|
43
|
+
buffer.pushError(error);
|
|
44
|
+
}
|
|
39
45
|
|
|
40
46
|
stream.on("data", (value) => {
|
|
41
47
|
debug("Received value from underlying Readable stream");
|
|
@@ -58,13 +64,14 @@ function wireReadable(debug, stream) {
|
|
|
58
64
|
if (!abortSeen) {
|
|
59
65
|
debug(`Underlying Readable stream produced an error: ${error.message}`);
|
|
60
66
|
abortSeen = true;
|
|
61
|
-
|
|
67
|
+
finishStream(ensureValidError(error));
|
|
62
68
|
}
|
|
63
69
|
});
|
|
64
70
|
|
|
65
71
|
stream.on("end", () => {
|
|
72
|
+
// TODO: How/where do we propagate Aborted markers, especially on subsequent overreads?
|
|
66
73
|
debug("Reached end of underlying Readable stream");
|
|
67
|
-
|
|
74
|
+
finishStream(new EndOfStream("The underlying stream has ended"));
|
|
68
75
|
});
|
|
69
76
|
|
|
70
77
|
return {
|
|
@@ -72,11 +79,16 @@ function wireReadable(debug, stream) {
|
|
|
72
79
|
read: async function () {
|
|
73
80
|
let promise = buffer.request();
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
let counts = buffer.countLane();
|
|
83
|
+
if (counts.requests > 0 && counts.values === 0) {
|
|
84
|
+
// We have run out of values
|
|
85
|
+
if (endError != null) {
|
|
86
|
+
buffer.pushError(endError);
|
|
87
|
+
} else if (isPaused) {
|
|
88
|
+
debug("Resuming underlying Readable stream, as our buffer ran out");
|
|
89
|
+
isPaused = false;
|
|
90
|
+
stream.resume();
|
|
91
|
+
}
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
return promise;
|
|
@@ -123,11 +135,26 @@ function isWritableStreamEnded(stream) {
|
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
function wireWritable(debug, stream) {
|
|
126
|
-
let errorEncountered;
|
|
138
|
+
let errorEncountered, lastAbort;
|
|
139
|
+
let aborted = false;
|
|
140
|
+
|
|
141
|
+
function abortPromistream(reason) {
|
|
142
|
+
/* If an abort/end of the underlying Node stream occurred for a reason that's entirely unrelated to our current pipeline - most notably, this happens if the error originates from the *read* side of a non-transform Duplex stream - then we want to make sure that we tear down the Promistream pipeline on this side too. */
|
|
143
|
+
if (lastAbort != null && !aborted) {
|
|
144
|
+
aborted = true;
|
|
145
|
+
lastAbort(reason);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
127
148
|
|
|
128
149
|
stream.on("error", (error) => {
|
|
129
150
|
debug("Logging writable interface error:", error);
|
|
130
151
|
errorEncountered = ensureValidError(error);
|
|
152
|
+
abortPromistream(error);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
stream.on("end", () => {
|
|
156
|
+
debug("Reached end of underlying Writable stream");
|
|
157
|
+
abortPromistream(true);
|
|
131
158
|
});
|
|
132
159
|
|
|
133
160
|
async function maybeWaitForDrain() {
|
|
@@ -149,6 +176,7 @@ function wireWritable(debug, stream) {
|
|
|
149
176
|
await maybeWaitForDrain();
|
|
150
177
|
if (!isStdioStream(stream) && !isWritableStreamEnded(stream)) {
|
|
151
178
|
debug("Calling .destroy on Writable stream");
|
|
179
|
+
aborted = true;
|
|
152
180
|
stream.destroy(error);
|
|
153
181
|
}
|
|
154
182
|
},
|
|
@@ -156,10 +184,13 @@ function wireWritable(debug, stream) {
|
|
|
156
184
|
await maybeWaitForDrain();
|
|
157
185
|
if (!isStdioStream(stream) && !isWritableStreamEnded(stream)) {
|
|
158
186
|
debug("Calling .end on Writable stream");
|
|
187
|
+
aborted = true;
|
|
159
188
|
stream.end();
|
|
160
189
|
}
|
|
161
190
|
},
|
|
162
191
|
tryWrite: async function (value, abort) {
|
|
192
|
+
lastAbort = abort;
|
|
193
|
+
|
|
163
194
|
if (isWritableStreamEnded(stream)) {
|
|
164
195
|
debug("Could not write because underlying stream has ended");
|
|
165
196
|
if (errorEncountered != null) {
|
|
@@ -227,7 +258,7 @@ function convertFromTransform(stream) {
|
|
|
227
258
|
// This is essentially a 'background process' of sorts, with manual error handling wiring.
|
|
228
259
|
try {
|
|
229
260
|
while (endMarker == null) {
|
|
230
|
-
await tryWrite(await lastSource.read());
|
|
261
|
+
await tryWrite(await lastSource.read(), lastSource.abort);
|
|
231
262
|
}
|
|
232
263
|
} catch (error) {
|
|
233
264
|
if (endMarker == null && (isEndOfStream(error) || isAborted(error))) { // FIXME: isExhausted?
|
package/test.js
CHANGED
|
@@ -16,6 +16,7 @@ const parallelize = require("@promistream/parallelize");
|
|
|
16
16
|
const testAbort = require("@promistream/test-abort");
|
|
17
17
|
const debug = require("@promistream/debug");
|
|
18
18
|
const { once } = require("node:events");
|
|
19
|
+
const spy = require("@promistream/spy");
|
|
19
20
|
|
|
20
21
|
test("readable stream", async () => {
|
|
21
22
|
let testFile = path.join(__dirname, "pnpm-lock.yaml"); // Big-ish file, so we're likely to read in multiple chunks
|
|
@@ -60,7 +61,6 @@ test("readable stream with happy abort", { timeout: 500 }, async () => {
|
|
|
60
61
|
assert.deepStrictEqual(result, []);
|
|
61
62
|
});
|
|
62
63
|
|
|
63
|
-
|
|
64
64
|
test("writable stream", async () => {
|
|
65
65
|
let buffer = Buffer.alloc(400000);
|
|
66
66
|
|
|
@@ -86,6 +86,34 @@ test("writable stream", async () => {
|
|
|
86
86
|
assert.strictEqual(endReached, true);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
test("writable stream with error abort", async () => {
|
|
90
|
+
let buffer = Buffer.alloc(400000);
|
|
91
|
+
|
|
92
|
+
let writableBuffer = Buffer.alloc(0);
|
|
93
|
+
let endReached = false;
|
|
94
|
+
let nodeWritable = new stream.Writable({
|
|
95
|
+
write(chunk, _encoding, callback) {
|
|
96
|
+
writableBuffer = Buffer.concat([ writableBuffer, chunk ]);
|
|
97
|
+
callback();
|
|
98
|
+
},
|
|
99
|
+
final(callback) {
|
|
100
|
+
endReached = true;
|
|
101
|
+
callback();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await assert.rejects(async () => {
|
|
106
|
+
await pipe([
|
|
107
|
+
fromIterable([ buffer, buffer, buffer ]),
|
|
108
|
+
testAbort(0, new Error("sample error")),
|
|
109
|
+
fromNodeStream(nodeWritable)
|
|
110
|
+
]).read();
|
|
111
|
+
}, { message: "sample error" });
|
|
112
|
+
|
|
113
|
+
assert.strictEqual(writableBuffer.length, 0);
|
|
114
|
+
assert.strictEqual(endReached, false);
|
|
115
|
+
});
|
|
116
|
+
|
|
89
117
|
test("transform stream", async () => {
|
|
90
118
|
let result = await pipe([
|
|
91
119
|
fromIterable([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]),
|
|
@@ -107,7 +135,7 @@ test("transform stream", async () => {
|
|
|
107
135
|
});
|
|
108
136
|
|
|
109
137
|
test("transform stream with abort", async () => {
|
|
110
|
-
assert.rejects(async () => {
|
|
138
|
+
await assert.rejects(async () => {
|
|
111
139
|
let pipeline = pipe([
|
|
112
140
|
fromIterable([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]),
|
|
113
141
|
fromNodeStream(new stream.Transform({
|
|
@@ -149,6 +177,48 @@ test("transform stream, parallel", async () => {
|
|
|
149
177
|
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 ]);
|
|
150
178
|
});
|
|
151
179
|
|
|
180
|
+
test("transform stream, overread", { timeout: 500 }, async () => {
|
|
181
|
+
let pipeline = pipe([
|
|
182
|
+
fromIterable([ 0 ]),
|
|
183
|
+
fromNodeStream(new stream.Transform({
|
|
184
|
+
objectMode: true,
|
|
185
|
+
transform(chunk, _encoding, callback) {
|
|
186
|
+
setTimeout(() => {
|
|
187
|
+
this.push(chunk);
|
|
188
|
+
this.push(chunk * 2);
|
|
189
|
+
callback();
|
|
190
|
+
}, 10);
|
|
191
|
+
}
|
|
192
|
+
}))
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
assert.strictEqual(await pipeline.read(), 0);
|
|
196
|
+
assert.strictEqual(await pipeline.read(), 0);
|
|
197
|
+
await assert.rejects(() => pipeline.read(), { name: "EndOfStream" });
|
|
198
|
+
await assert.rejects(() => pipeline.read(), { name: "EndOfStream" }); /* This should repeat the last error, just like a normal source stream would */
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("transform stream, abort overread", { timeout: 500 }, async () => {
|
|
202
|
+
let pipeline = pipe([
|
|
203
|
+
fromIterable([ 0 ]),
|
|
204
|
+
testAbort(0, new Error("sample error")),
|
|
205
|
+
fromNodeStream(new stream.Transform({
|
|
206
|
+
objectMode: true,
|
|
207
|
+
transform(chunk, _encoding, callback) {
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
this.push(chunk);
|
|
210
|
+
this.push(chunk * 2);
|
|
211
|
+
callback();
|
|
212
|
+
}, 10);
|
|
213
|
+
}
|
|
214
|
+
}))
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
/* Since we're reading directly from the pipeline, we will get an Aborted marker in both cases; it is normally the sink stream that is responsible for unpacking and throwing the original cause, but we don't have a sink stream in this case. */
|
|
218
|
+
await assert.rejects(() => pipeline.read(), { name: "Aborted", message: "Stream was aborted due to error: sample error" });
|
|
219
|
+
await assert.rejects(() => pipeline.read(), { name: "Aborted", message: "Stream was aborted due to error: sample error" });
|
|
220
|
+
});
|
|
221
|
+
|
|
152
222
|
/* FIXME: Do network socket test with both happy abort and error abort */
|
|
153
223
|
/* TODO: Flaky test behaviour was observed for the below test:
|
|
154
224
|
|
|
@@ -203,3 +273,50 @@ test("network socket", async () => {
|
|
|
203
273
|
server.close();
|
|
204
274
|
}
|
|
205
275
|
});
|
|
276
|
+
|
|
277
|
+
async function wait10() {
|
|
278
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
test("network socket with error abort", { timeout: 500 }, async () => {
|
|
282
|
+
let { resolve, promise } = pDefer();
|
|
283
|
+
|
|
284
|
+
let server = net.createServer((socket) => {
|
|
285
|
+
let serverRead = fromNodeStream.fromReadable(socket);
|
|
286
|
+
let serverWrite = fromNodeStream.fromWritable(socket);
|
|
287
|
+
resolve({ serverRead, serverWrite });
|
|
288
|
+
});
|
|
289
|
+
server.listen(14292);
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
let client = net.connect({ host: "localhost", port: 14292 });
|
|
293
|
+
let clientRead = fromNodeStream.fromReadable(client);
|
|
294
|
+
let clientWrite = fromNodeStream.fromWritable(client);
|
|
295
|
+
let { serverRead, serverWrite } = await promise;
|
|
296
|
+
|
|
297
|
+
let serverPipe = pipe([ serverRead, collect() ]);
|
|
298
|
+
let serverWritePipe = pipe([
|
|
299
|
+
fromIterable([ Buffer.from("c"), Buffer.from("d") ]),
|
|
300
|
+
testAbort(0, new Error("sample error")),
|
|
301
|
+
serverWrite
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
let clientPipe = pipe([ clientRead, collect() ]);
|
|
305
|
+
let clientWritePipe = pipe([
|
|
306
|
+
fromIterable([ Buffer.from("a"), Buffer.from("b") ]),
|
|
307
|
+
/* Give the client side some time to notice that the server side closed the connection */
|
|
308
|
+
spy(async(value) => { await wait10(); return value; }),
|
|
309
|
+
clientWrite
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
let results = await Promise.all([
|
|
313
|
+
assert.rejects(() => serverPipe.read(), { message: "sample error" }),
|
|
314
|
+
assert.rejects(() => serverWritePipe.read(), { message: "sample error" }),
|
|
315
|
+
/* The other side of the connection closes normally, like it would for any disconnection */
|
|
316
|
+
clientPipe.read(),
|
|
317
|
+
clientWritePipe.read(),
|
|
318
|
+
]);
|
|
319
|
+
} finally {
|
|
320
|
+
server.close();
|
|
321
|
+
}
|
|
322
|
+
});
|