UNPKG

@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
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. */ @Memoize() 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(); } } }