@clickup/rest-client
Version:
A syntax sugar tool around Node fetch() API, tailored to work with TypeScript and response validators
90 lines (82 loc) • 2.95 kB
text/typescript
import { Memoize } from "fast-typescript-memoize";
import type RestResponse from "./RestResponse";
/**
* Once created, RestStream must be iterated in full, otherwise the connection
* will remain dangling. Also, this class is where we hide the details of the
* actual stream reading using AsyncGenerator bridge abstraction.
*
* RestStream can also read binary data depending on the Content-Type response
* header and/or the charset provided there. The binary data is still returned
* as a string, one string character per each byte. To convert it to a Buffer,
* use something like `Buffer.from(responseText, "binary")`.
*/
export default class RestStream {
private _generator?: AsyncGenerator<string, void>;
constructor(
public readonly res: RestResponse,
readerIterable: {
[Symbol.asyncIterator]: () => AsyncGenerator<string, void>;
},
) {
this._generator = readerIterable[Symbol.asyncIterator]();
}
/**
* Reads the prefix of the stream. Closes the connection after the read is
* done in all cases, so safe to be used to e.g. receive a trimmed response.
*/
async consumeReturningPrefix(maxChars: number): Promise<string> {
const text: string[] = [];
let length = 0;
for await (const chunk of this) {
// According to Google, in v8 string concatenation is as efficient as
// array or buffer joining.
text.push(chunk);
length += chunk.length;
if (length >= maxChars) {
break;
}
}
return text.join("").substring(0, maxChars);
}
/**
* Closes the connection.
*/
async close(): Promise<void> {
// First, try to interrupt the active iteration, if any.
await this[Symbol.asyncIterator]().return();
// It is possible that this.[Symbol.asyncIterator] has never been iterated
// before at all, so its `finally` got never executed. This happens when
// RestFetchReader#preload() consumed some small chunk of data, and then
// no-one iterated the remaining body in RestStream, they just called
// close() on it. So we recheck & close the RestFetchReader iterator if it
// is still there.
const generator = this._generator;
if (generator) {
delete this._generator;
await generator.return();
}
}
/**
* Allows to iterate over the entire stream of data. You must consume the
* entire iterable or at least call this.close(), otherwise the connection may
* remain open.
*/
()
async *[Symbol.asyncIterator](): AsyncGenerator<string, void> {
const generator = this._generator;
if (!generator) {
return;
}
delete this._generator;
try {
yield this.res.text;
for await (const chunk of generator) {
yield chunk;
}
} finally {
// The code enters here if the caller interrupted (returned) the iteration
// after receiving the 1st textFetched chunk.
await generator.return();
}
}
}