Object.defineProperty(exports, '__esModule', { value: true }); const getStreamContents = async (stream, {init, convertChunk, getSize, truncateChunk, addChunk, getFinalChunk, finalize}, {maxBuffer = Number.POSITIVE_INFINITY} = {}) => { if (!isAsyncIterable(stream)) { throw new Error('The first argument must be a Readable, a ReadableStream, or an async iterable.'); } const state = init(); state.length = 0; try { for await (const chunk of stream) { const chunkType = getChunkType(chunk); const convertedChunk = convertChunk[chunkType](chunk, state); appendChunk({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}); } appendFinalChunk({state, convertChunk, getSize, truncateChunk, addChunk, getFinalChunk, maxBuffer}); return finalize(state); } catch (error) { error.bufferedData = finalize(state); throw error; } }; const appendFinalChunk = ({state, getSize, truncateChunk, addChunk, getFinalChunk, maxBuffer}) => { const convertedChunk = getFinalChunk(state); if (convertedChunk !== undefined) { appendChunk({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}); } }; const appendChunk = ({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}) => { const chunkSize = getSize(convertedChunk); const newLength = state.length + chunkSize; if (newLength <= maxBuffer) { addNewChunk(convertedChunk, state, addChunk, newLength); return; } const truncatedChunk = truncateChunk(convertedChunk, maxBuffer - state.length); if (truncatedChunk !== undefined) { addNewChunk(truncatedChunk, state, addChunk, maxBuffer); } throw new MaxBufferError(); }; const addNewChunk = (convertedChunk, state, addChunk, newLength) => { state.contents = addChunk(convertedChunk, state, newLength); state.length = newLength; }; const isAsyncIterable = stream => typeof stream === 'object' && stream !== null && typeof stream[Symbol.asyncIterator] === 'function'; const getChunkType = chunk => { const typeOfChunk = typeof chunk; if (typeOfChunk === 'string') { return 'string'; } if (typeOfChunk !== 'object' || chunk === null) { return 'others'; } // eslint-disable-next-line n/prefer-global/buffer if (globalThis.Buffer?.isBuffer(chunk)) { return 'buffer'; } const prototypeName = objectToString.call(chunk); if (prototypeName === '[object ArrayBuffer]') { return 'arrayBuffer'; } if (prototypeName === '[object DataView]') { return 'dataView'; } if ( Number.isInteger(chunk.byteLength) && Number.isInteger(chunk.byteOffset) && objectToString.call(chunk.buffer) === '[object ArrayBuffer]' ) { return 'typedArray'; } return 'others'; }; const {toString: objectToString} = Object.prototype; class MaxBufferError extends Error { name = 'MaxBufferError'; constructor() { super('maxBuffer exceeded'); } } const identity = value => value; const noop = () => undefined; const getContentsProp = ({contents}) => contents; const throwObjectStream = chunk => { throw new Error(`Streams in object mode are not supported: ${String(chunk)}`); }; const getLengthProp = convertedChunk => convertedChunk.length; async function getStreamAsArray(stream, options) { return getStreamContents(stream, arrayMethods, options); } const initArray = () => ({contents: []}); const increment = () => 1; const addArrayChunk = (convertedChunk, {contents}) => { contents.push(convertedChunk); return contents; }; const arrayMethods = { init: initArray, convertChunk: { string: identity, buffer: identity, arrayBuffer: identity, dataView: identity, typedArray: identity, others: identity, }, getSize: increment, truncateChunk: noop, addChunk: addArrayChunk, getFinalChunk: noop, finalize: getContentsProp, }; async function getStreamAsArrayBuffer(stream, options) { return getStreamContents(stream, arrayBufferMethods, options); } const initArrayBuffer = () => ({contents: new ArrayBuffer(0)}); const useTextEncoder = chunk => textEncoder.encode(chunk); const textEncoder = new TextEncoder(); const useUint8Array = chunk => new Uint8Array(chunk); const useUint8ArrayWithOffset = chunk => new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); const truncateArrayBufferChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize); // `contents` is an increasingly growing `Uint8Array`. const addArrayBufferChunk = (convertedChunk, {contents, length: previousLength}, length) => { const newContents = hasArrayBufferResize() ? resizeArrayBuffer(contents, length) : resizeArrayBufferSlow(contents, length); new Uint8Array(newContents).set(convertedChunk, previousLength); return newContents; }; // Without `ArrayBuffer.resize()`, `contents` size is always a power of 2. // This means its last bytes are zeroes (not stream data), which need to be // trimmed at the end with `ArrayBuffer.slice()`. const resizeArrayBufferSlow = (contents, length) => { if (length <= contents.byteLength) { return contents; } const arrayBuffer = new ArrayBuffer(getNewContentsLength(length)); new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0); return arrayBuffer; }; // With `ArrayBuffer.resize()`, `contents` size matches exactly the size of // the stream data. It does not include extraneous zeroes to trim at the end. // The underlying `ArrayBuffer` does allocate a number of bytes that is a power // of 2, but those bytes are only visible after calling `ArrayBuffer.resize()`. const resizeArrayBuffer = (contents, length) => { if (length <= contents.maxByteLength) { contents.resize(length); return contents; } const arrayBuffer = new ArrayBuffer(length, {maxByteLength: getNewContentsLength(length)}); new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0); return arrayBuffer; }; // Retrieve the closest `length` that is both >= and a power of 2 const getNewContentsLength = length => SCALE_FACTOR ** Math.ceil(Math.log(length) / Math.log(SCALE_FACTOR)); const SCALE_FACTOR = 2; const finalizeArrayBuffer = ({contents, length}) => hasArrayBufferResize() ? contents : contents.slice(0, length); // `ArrayBuffer.slice()` is slow. When `ArrayBuffer.resize()` is available // (Node >=20.0.0, Safari >=16.4 and Chrome), we can use it instead. // eslint-disable-next-line no-warning-comments // TODO: remove after dropping support for Node 20. // eslint-disable-next-line no-warning-comments // TODO: use `ArrayBuffer.transferToFixedLength()` instead once it is available const hasArrayBufferResize = () => 'resize' in ArrayBuffer.prototype; const arrayBufferMethods = { init: initArrayBuffer, convertChunk: { string: useTextEncoder, buffer: useUint8Array, arrayBuffer: useUint8Array, dataView: useUint8ArrayWithOffset, typedArray: useUint8ArrayWithOffset, others: throwObjectStream, }, getSize: getLengthProp, truncateChunk: truncateArrayBufferChunk, addChunk: addArrayBufferChunk, getFinalChunk: noop, finalize: finalizeArrayBuffer, }; async function getStreamAsBuffer(stream, options) { if (!('Buffer' in globalThis)) { throw new Error('getStreamAsBuffer() is only supported in Node.js'); } try { return arrayBufferToNodeBuffer(await getStreamAsArrayBuffer(stream, options)); } catch (error) { if (error.bufferedData !== undefined) { error.bufferedData = arrayBufferToNodeBuffer(error.bufferedData); } throw error; } } // eslint-disable-next-line n/prefer-global/buffer const arrayBufferToNodeBuffer = arrayBuffer => globalThis.Buffer.from(arrayBuffer); async function getStreamAsString(stream, options) { return getStreamContents(stream, stringMethods, options); } const initString = () => ({contents: '', textDecoder: new TextDecoder()}); const useTextDecoder = (chunk, {textDecoder}) => textDecoder.decode(chunk, {stream: true}); const addStringChunk = (convertedChunk, {contents}) => contents + convertedChunk; const truncateStringChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize); const getFinalStringChunk = ({textDecoder}) => { const finalChunk = textDecoder.decode(); return finalChunk === '' ? undefined : finalChunk; }; const stringMethods = { init: initString, convertChunk: { string: identity, buffer: useTextDecoder, arrayBuffer: useTextDecoder, dataView: useTextDecoder, typedArray: useTextDecoder, others: throwObjectStream, }, getSize: getLengthProp, truncateChunk: truncateStringChunk, addChunk: addStringChunk, getFinalChunk: getFinalStringChunk, finalize: getContentsProp, }; exports.MaxBufferError = MaxBufferError; exports.default = getStreamAsString; exports.getStreamAsArray = getStreamAsArray; exports.getStreamAsArrayBuffer = getStreamAsArrayBuffer; exports.getStreamAsBuffer = getStreamAsBuffer;