Skip to content

Commit 4bb8a9f

Browse files
committed
Merge pull request #569 from lifengl/originSpinFix
I took liberties to squash/rewrite commits, making minimal changes to the original proposed change.
2 parents 6885575 + 6e8c2db commit 4bb8a9f

File tree

5 files changed

+100
-23
lines changed

5 files changed

+100
-23
lines changed

src/nerdbank-streams/src/MultiplexingStreamFormatters.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,11 +299,17 @@ export class MultiplexingStreamV2Formatter extends MultiplexingStreamFormatter {
299299
const readObject = this.reader.read();
300300
if (readObject === null) {
301301
const bytesAvailable = new Deferred<void>();
302-
this.reader.once("readable", bytesAvailable.resolve.bind(bytesAvailable));
303-
this.reader.once("end", streamEnded.resolve.bind(streamEnded));
302+
const bytesAvailableCallback = bytesAvailable.resolve.bind(bytesAvailable);
303+
const streamEndedCallback = streamEnded.resolve.bind(streamEnded);
304+
305+
this.reader.once("readable", bytesAvailableCallback);
306+
this.reader.once("end", streamEndedCallback);
304307
const endPromise = Promise.race([bytesAvailable.promise, streamEnded.promise]);
305308
await (cancellationToken ? cancellationToken.racePromise(endPromise) : endPromise);
306309

310+
this.reader.removeListener("readable", bytesAvailableCallback);
311+
this.reader.removeListener("end", streamEndedCallback);
312+
307313
if (bytesAvailable.isCompleted) {
308314
continue;
309315
}

src/nerdbank-streams/src/Utilities.ts

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,32 +72,80 @@ export async function getBufferFrom(
7272
cancellationToken?: CancellationToken): Promise<Buffer | null> {
7373

7474
const streamEnded = new Deferred<void>();
75-
while (size > 0) {
76-
cancellationToken?.throwIfCancelled();
7775

78-
const readBuffer = readable.read(size) as Buffer;
79-
if (readBuffer === null) {
80-
const bytesAvailable = new Deferred<void>();
81-
readable.once("readable", bytesAvailable.resolve.bind(bytesAvailable));
82-
readable.once("end", streamEnded.resolve.bind(streamEnded));
83-
const endPromise = Promise.race([bytesAvailable.promise, streamEnded.promise]);
84-
await (cancellationToken ? cancellationToken.racePromise(endPromise) : endPromise);
76+
if (size === 0) {
77+
return Buffer.from([]);
78+
}
8579

86-
if (bytesAvailable.isCompleted) {
87-
continue;
80+
let readBuffer: Buffer | null = null;
81+
let index: number = 0;
82+
while (size > 0) {
83+
cancellationToken?.throwIfCancelled();
84+
let availableSize = (readable as Readable).readableLength;
85+
if (!availableSize) {
86+
// Check the end of stream
87+
if ((readable as Readable).readableEnded || streamEnded.isCompleted) {
88+
// stream is closed
89+
if (!allowEndOfStream) {
90+
throw new Error("Stream terminated before required bytes were read.");
91+
}
92+
93+
// Returns what has been read so far
94+
if (readBuffer === null) {
95+
return null;
96+
}
97+
98+
// we need trim extra spaces
99+
return readBuffer.subarray(0, index)
88100
}
101+
102+
// we retain this behavior when availableSize === false
103+
// to make existing unit tests happy (which assumes we will try to read stream when no data is ready.)
104+
availableSize = size;
105+
} else if (availableSize > size) {
106+
availableSize = size;
89107
}
90108

91-
if (!allowEndOfStream) {
92-
if (!readBuffer || readBuffer.length < size) {
109+
const newBuffer = readable.read(availableSize) as Buffer;
110+
if (newBuffer) {
111+
if (newBuffer.length < availableSize && !allowEndOfStream) {
93112
throw new Error("Stream terminated before required bytes were read.");
94113
}
114+
115+
if (readBuffer === null) {
116+
if (availableSize === size || newBuffer.length < availableSize) {
117+
// in the fast pass, we read the entire data once, and donot allocate an extra array.
118+
return newBuffer;
119+
}
120+
121+
// if we read partial data, we need allocate a buffer to join all data together.
122+
readBuffer = Buffer.alloc(size);
123+
}
124+
125+
// now append new data to the buffer
126+
newBuffer.copy(readBuffer, index);
127+
128+
size -= newBuffer.length;
129+
index += newBuffer.length;
95130
}
96131

97-
return readBuffer;
132+
if (size > 0) {
133+
const bytesAvailable = new Deferred<void>();
134+
const bytesAvailableCallback = bytesAvailable.resolve.bind(bytesAvailable);
135+
const streamEndedCallback = streamEnded.resolve.bind(streamEnded);
136+
readable.once("readable", bytesAvailableCallback);
137+
readable.once("end", streamEndedCallback);
138+
try {
139+
const endPromise = Promise.race([bytesAvailable.promise, streamEnded.promise]);
140+
await (cancellationToken ? cancellationToken.racePromise(endPromise) : endPromise);
141+
} finally {
142+
readable.removeListener("readable", bytesAvailableCallback);
143+
readable.removeListener("end", streamEndedCallback);
144+
}
145+
}
98146
}
99147

100-
return Buffer.from([]);
148+
return readBuffer;
101149
}
102150

103151
export function throwIfDisposed(value: IDisposableObservable) {

src/nerdbank-streams/src/tests/FullDuplexStream.spec.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { PassThrough, Readable, Writable } from "stream";
22
import { Deferred } from "../Deferred";
33
import { FullDuplexStream } from "../FullDuplexStream";
44
import { getBufferFrom } from "../Utilities";
5+
import { delay } from "./Timeout";
56

67
describe("FullDuplexStream.CreatePair", () => {
78

@@ -62,8 +63,8 @@ describe("FullDuplexStream.Splice", () => {
6263
let duplex: NodeJS.ReadWriteStream;
6364

6465
beforeEach(() => {
65-
readable = new PassThrough();
66-
writable = new PassThrough();
66+
readable = new PassThrough({ writableHighWaterMark : 8 });
67+
writable = new PassThrough({ writableHighWaterMark : 8 });
6768
duplex = FullDuplexStream.Splice(readable, writable);
6869
});
6970

@@ -86,4 +87,18 @@ describe("FullDuplexStream.Splice", () => {
8687
buffer = await getBufferFrom(writable, 1, true);
8788
expect(buffer).toBeNull();
8889
});
90+
91+
it("Read should yield when data is not ready", async () => {
92+
const task = writeToStream(duplex, "abcdefgh", 4);
93+
const buffer = await getBufferFrom(writable, 32);
94+
await task;
95+
expect(buffer.length).toEqual(32);
96+
});
97+
98+
async function writeToStream(stream: NodeJS.ReadWriteStream, message: string, repeat: number) {
99+
while (repeat--) {
100+
stream.write(message);
101+
await delay(2);
102+
}
103+
}
89104
});

src/nerdbank-streams/src/tests/MultiplexingStream.Interop.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,13 @@ import { ChannelOptions } from "../ChannelOptions";
107107
if (readBuffer === null) {
108108
const bytesAvailable = new Deferred<void>();
109109
const streamEnded = new Deferred<void>();
110-
readable.once("readable", bytesAvailable.resolve.bind(bytesAvailable));
111-
readable.once("end", streamEnded.resolve.bind(streamEnded));
110+
const bytesAvailableCallback = bytesAvailable.resolve.bind(bytesAvailable);
111+
const streamEndedCallback = streamEnded.resolve.bind(streamEnded);
112+
readable.once("readable", bytesAvailableCallback);
113+
readable.once("end", streamEndedCallback);
112114
await Promise.race([bytesAvailable.promise, streamEnded.promise]);
115+
readable.removeListener("readable", bytesAvailableCallback);
116+
readable.removeListener("end", streamEndedCallback);
113117
if (bytesAvailable.isCompleted) {
114118
readBuffer = readable.read() as Buffer;
115119
} else {

src/nerdbank-streams/src/tests/MultiplexingStream.SeededChannels.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,13 @@ async function readAsync(readable: NodeJS.ReadableStream): Promise<Buffer | null
7979
if (readBuffer === null) {
8080
const bytesAvailable = new Deferred<void>();
8181
const streamEnded = new Deferred<void>();
82-
readable.once("readable", bytesAvailable.resolve.bind(bytesAvailable));
83-
readable.once("end", streamEnded.resolve.bind(streamEnded));
82+
const bytesAvailableCallback = bytesAvailable.resolve.bind(bytesAvailable);
83+
const streamEndedCallback = streamEnded.resolve.bind(streamEnded);
84+
readable.once("readable", bytesAvailableCallback);
85+
readable.once("end", streamEndedCallback);
8486
await Promise.race([bytesAvailable.promise, streamEnded.promise]);
87+
readable.removeListener("readable", bytesAvailableCallback);
88+
readable.removeListener("end", streamEndedCallback);
8589
if (bytesAvailable.isCompleted) {
8690
readBuffer = readable.read() as Buffer;
8791
} else {

0 commit comments

Comments
 (0)